REST API reference
All /api/v1/β¦ endpoints with examples.
Every UI surface MeshHold ships β the bundled Web UI, the Android app, the
Windows tray launcher, the meshhold CLI's --api family β is a client of
this REST API. There are no "internal" endpoints; if the Web UI does it, you
can do it too with curl.
The base URL is whatever the daemon binds (api.bind in config.yaml,
default 127.0.0.1:8080):
http://127.0.0.1:8080/api/v1/...
When api.cert_file / api.key_file are set β or when the daemon
auto-generates a self-signed pair on first start β the same prefix moves to
https://. The TLS material is also exposed via /system/status so a
freshly-paired client can pin the fingerprint.
The surface clusters into six themes β storage, mesh control, comms, networking, integration, observability. Every section below maps to one of them; the diagram is a "you are here" key for the rest of this page.
Authentication
All routes live behind a bearer token. The token is minted by the login endpoint and presented on every subsequent call:
Authorization: Bearer <token>
Five endpoints are open without a token (for bootstrap reasons):
/version, /system/status, /auth/login, /auth/logout,
/auth/bootstrap-exchange, and /auth/admin/set-password (which enforces
its own X-MeshHold-Admin-Token check instead). Inbound webhooks at
/hooks/<token> are also exempt β they carry their secret in the path.
POST /auth/login
Exchange the Web UI password for a 24 h bearer.
curl -s http://127.0.0.1:8080/api/v1/auth/login \
-H 'content-type: application/json' \
-d '{"password":"hunter2"}'
{"token":"7f3bβ¦","expires_at_ms":1716595200000}
Failure modes: 503 when no password has ever been set, 401 on a wrong
password (with a deliberate ~100 ms delay to slow guessing).
POST /auth/bootstrap-exchange
Trade a one-shot bootstrap ticket β minted by the tray launcher via the
loopback IPC pipe β for a regular 24 h bearer. The Web UI hits this
automatically when it spots a ?bootstrap=β¦ query parameter on first
paint. Tickets are single-use and expire 60 s after issue.
POST /auth/admin/set-password
CLI-only. Replaces the Web UI password without knowing the current one.
The caller proves authority by presenting the daemon's process-lifetime
token from <metadata_dir>/cli-admin.token (mode 0600) in the
X-MeshHold-Admin-Token header. There is no web surface for this.
POST /auth/logout
Invalidates the bearer. 204 No Content either way.
Query-string token (?token=β¦)
A few routes accept the bearer in ?token= instead of the header, for
browser primitives that can't set custom headers:
| Path | Why |
|---|---|
GET /calls/events |
EventSource |
GET /events/stream |
EventSource |
GET /sessions/{sid}/events |
EventSource (AI agents) |
GET /sessions/{sid}/files |
<a download> / <img src> |
GET /audit/export |
<a download> |
GET /diagnostics/reports/{id} |
<a download> |
GET /calls/{id}/media |
WebSocket constructor |
GET /rooms/{id}/stream |
WebSocket constructor |
GET /vaults/{id}/stream/... |
<video src> |
GET /vaults/{id}/cover/... |
<img src> |
GET /vaults/{id}/subtitle/... |
<track src> |
Conventions
- All payloads are JSON unless stated otherwise.
Content-Type: application/jsonis mandatory on writes. - Time values are milliseconds since epoch as
int64, fields suffixed_ms. Durations are Go-style strings ("5s","2m"). - Vault and node identifiers are opaque strings. Tree-shaped paths
(
*pathroutes) are URL-encoded; a leading/is required. - Errors always look like
{"error":"message"}with a non-2xx status.507 Insufficient Storageis reserved for vault hard-quota hits and carriesvault_id/hard_quota/would_be_sizealongside the message.
System
The "what is this daemon" surface. Two of these (/version,
/system/status) bypass auth so a launcher can introspect before logging
in.
| Method | Path | Notes |
|---|---|---|
| GET | /version |
{ "version": "0.7.63" }. Cheap, unauth. |
| GET | /whoami |
{ "node_id": "12D3KooWβ¦" }. Local peer ID. |
| GET | /system/status |
Full health snapshot β see below. Unauth. |
| POST | /system/setup |
First-boot helper β accepts a swarm key + bootstrap list and writes them into config.yaml. |
| GET | /system/share |
swarm_key, bootstrap, connect_url (a meshhold://join/β¦ URL) for onboarding a new device. |
| GET | /system/share-node |
Same idea but without the swarm key β just node identity + reachable hosts. |
| GET | /system/metrics |
24 h ring buffer of per-minute metrics. ?from=<ms>&to=<ms> narrows the window. |
| GET | /system/browse |
Server-side filesystem browser used by the "pick a folder" dialog when adding a storage vault. |
| GET | /system/nat |
Diagnostics for the UPnP IGD subsystem + the /api/ipinfo lookup. Powers the Settings β Router & NAT panel. |
GET /system/status is the canonical health probe; the response includes
running, has_swarm_key, setup_required, peer_id, listen_addrs,
s3_enabled, tls.fingerprint, at_rest.{enabled,provider}, and the
active network's {state,id,name}.
Networks
A "network" is a saved (swarm key, bootstrap list) pair. The daemon can hold many but is only ever active in one.
| Method | Path | Notes |
|---|---|---|
| GET | /networks |
List saved networks plus the currently-active one. |
| POST | /networks |
Add an existing network from a meshhold://join/β¦ URL. |
| POST | /networks/create |
Mint a brand-new swarm key and add as a network. |
| DELETE | /networks/:id |
Forget. The active network can't be deleted while connected. |
| POST | /networks/:id/connect |
Switch the daemon onto this network (P2P stack restarts). |
| POST | /networks/disconnect |
Tear down P2P; daemon stays alive in offline mode. |
| GET | /networks/:id/invite |
Build a fresh meshhold://join/β¦ URL for sharing. |
| GET | /networks/:id/key |
Return the raw swarm key text (3-line IPFS PSK format). |
| POST | /networks/:id/peers |
Append additional bootstrap multiaddrs from a share payload. |
| PUT | /networks/:id/bootstrap |
Replace the bootstrap list with the supplied multiaddrs. |
| PUT | /networks/:id/bootstrap-invite |
Same, but extract the multiaddrs from a meshhold:// URL. |
| POST | /networks/:id/swarm-key |
Rotate the PSK and push the new one to every connected peer. |
Vaults
Vaults are the storage unit β every file lives in exactly one. Trusted vaults hold a key locally; untrusted vaults read encrypted blocks from peers.
| Method | Path | Notes |
|---|---|---|
| GET | /vaults |
List all visible vaults (trusted + untrusted observed in the swarm). |
| GET | /vaults/:vault_id |
Vault metadata: name, key state, RF target, byte count, peer count. |
| GET | /vaults/:vault_id/info |
Heavier view with config-effective fields and replica health. |
| POST | /vaults |
Create a trusted vault. |
| PATCH | /vaults/:vault_id |
Mutate name / RF / hard quota / role flags. |
| DELETE | /vaults/:vault_id |
Untrust + remove from config. Blocks stay until eviction reclaims. |
| POST | /vaults/join |
Adopt a vault from a meshhold://vault/β¦ share URL. |
| GET | /vaults/:vault_id/share-url |
Mint a share URL for the vault (encodes key + reachable bootstrap). |
| GET | /vaults/:vault_id/scan-status |
Filesystem-scanner progress: {active, walked, ingested, errors, β¦}. |
| POST | /vaults/:vault_id/lock / /vaults/unlock |
Password-protected vaults β try a password against every locked vault. |
| GET | /vaults/:vault_id/analytics |
Top types, top consumers, history. Drives the per-vault analytics tab. |
| PUT | /vaults/:vault_id/s3-alias |
Set the bucket alias used by the S3 gateway for this vault. |
| POST | /replicate-now |
Kick a one-shot replication cycle. Returns immediately. |
Files
*path segments are URL-encoded and must begin with /.
| Method | Path | Notes |
|---|---|---|
| GET | /vaults/:vault_id/files |
List a vault. ?path=/folder filters by prefix; ?include_deleted=1. |
| GET | /vaults/:vault_id/files/*path |
One file's metadata (block hashes, parent hash, conflicts). |
| GET | /vaults/:vault_id/download/*path |
Stream the body. Sets Content-Disposition: attachment. |
| GET | /vaults/:vault_id/stream/*path |
Same body, no attachment, supports Range: headers for the player. |
| POST | /vaults/:vault_id/upload/*path |
Body is the raw file. Up to 50 GiB per request. Returns the new FileJSON. |
| DELETE | /vaults/:vault_id/files/*path |
Tombstone the file. Blocks are GC'd by the replication cycle. |
| GET | /vaults/:vault_id/info/*path |
File info: replicas (peer ID + display name), full version DAG. |
| GET | /vaults/:vault_id/history/*path |
Version list, newest first. |
| POST | /vaults/:vault_id/restore/*path |
Roll the head pointer to a prior content_hash (body {"content_hash"}). |
| GET | /vaults/:vault_id/version/*path?content_hash=β¦ |
Download a specific historical version. |
| GET | /vaults/:vault_id/locality/*path |
{local: bool, holders: [peer_idβ¦]} β used by the offline-mode badge. |
| POST | /vaults/:vault_id/cache/*path |
Force-fetch a file from peers so it's resident locally. |
| POST | /vaults/:vault_id/resolve-conflict/*path |
Pick the surviving leaf in a divergent version DAG. |
The upload response is synchronous β the client gets a FileJSON as soon
as the bytes are durably on disk and the catalog row is committed.
Replication to other nodes is a separate, asynchronous concern driven by
the broker; POST /replicate-now kicks it out of its sleep window.
# Upload a file to a vault:
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
--data-binary @./photo.jpg \
"http://127.0.0.1:8080/api/v1/vaults/$VID/upload/2026/photo.jpg"
# Download with Range β the player uses this verbatim:
curl -s -H "Authorization: Bearer $TOKEN" -H 'Range: bytes=0-1023' \
"http://127.0.0.1:8080/api/v1/vaults/$VID/stream/movie.mkv" -o head.bin
A vault hard-quota hit returns 507 Insufficient Storage:
{"error":"vault quota exceeded", "vault_id":"β¦",
"hard_quota":107374182400, "would_be_size":108221145600}
Per-file metadata, audio + video enrichment
| Method | Path | Notes |
|---|---|---|
| GET | /vaults/:vault_id/meta/*path |
Tags, audio fingerprint result, NFO, cover sha. |
| PUT | /vaults/:vault_id/meta/*path |
Manual override of selected meta fields. |
| POST | /vaults/:vault_id/refingerprint/*path |
Re-run AcoustID lookup on one file. |
| POST | /vaults/:vault_id/refresh-folder[/*path] |
Re-scan a folder for added/removed sidecars. |
| POST | /vaults/:vault_id/refingerprint-folder[/*path] |
Re-fingerprint every audio file under a folder. |
| POST | /vaults/:vault_id/audio/rescan |
Fire-and-forget rescan of every audio file in the vault. |
| GET | /vaults/:vault_id/audio/rescan/status |
{active, processed, total} for the rescan above. |
| GET | /vaults/:vault_id/cover/:hash |
Raw cover-art bytes. Cached aggressively. |
| GET | /vaults/:vault_id/video-meta/*path |
TMDB-enriched view: title, year, plot, poster path. |
| PUT | /vaults/:vault_id/video-meta/*path |
Override the auto-resolved TMDB match. |
| POST | /vaults/:vault_id/refingerprint-video/*path |
Re-run filename β TMDB lookup. |
| POST | /vaults/:vault_id/video-play-event/*path |
Push a "watched up to N ms" beat for resume. |
| GET | /vaults/:vault_id/video-tracks/*path |
Audio + subtitle track list (ffprobe + sidecar walk). |
| POST | /vaults/:vault_id/rescan-tracks/*path |
Invalidate the cached track list for this file. |
| GET | /vaults/:vault_id/subtitle/*path?track=β¦&format=vtt |
WebVTT-converted subtitle stream for the HTML5 <track>. |
Library (music)
The library endpoints aggregate the audio fingerprinting results across
every trusted vault into an iTunes-shaped view. Most accept
?fully_local=true to hide tracks the local node doesn't fully hold.
| Method | Path | Notes |
|---|---|---|
| GET | /library/artists |
Distinct artists with track counts. |
| GET | /library/artists/:name |
One artist's albums + songs. |
| GET | /library/albums |
Album grid (cover hash, year, song count). |
| GET | /library/albums/:key |
Album detail with tracklist. |
| GET | /library/songs |
Flat song list. Supports ?sort= and pagination. |
| GET | /library/genres |
Genre histogram. |
| GET | /library/search?q=β¦ |
Full-text across artist/album/title/genre. |
| GET | /library/prefs/:vault_id/:file_id |
Per-track user prefs (rating, skip). |
| PUT | /library/prefs/:vault_id/:file_id |
Update prefs. |
| POST | /play-events |
Append play/skip events; drives play counts. |
| POST | /library/radio |
Build a radio queue from a seed (?only_local=true). |
Network + nodes
| Method | Path | Notes |
|---|---|---|
| GET | /nodes |
Every peer seen via gossip β display name, country, caps, vault list. |
| GET | /nodes/self |
Same shape, just for this daemon. Useful when listing isn't. |
| GET | /nodes/:node_id |
One node's detail. |
| GET | /network/topology |
Gossip-aggregated mesh view (nodes + edges + uptime). |
| POST | /network/speedtest/:peer_id |
Run a latency + throughput probe against a peer. |
| GET | /network/speedtest/:peer_id/history |
Last N probe results. |
| GET | /stats/throughput |
10-minute ring (10 s buckets) of in/out byte rates. |
Rooms (chat)
room_id is the deterministic hash of the chat key + initial members.
| Method | Path | Notes |
|---|---|---|
| GET | /rooms |
All joined rooms with unread counts. |
| GET | /rooms/:room_id/messages?before=&limit= |
Paginated history, newest first. |
| POST | /rooms/:room_id/messages |
Send {"text":"β¦"}. |
| POST | /rooms/:room_id/messages/file |
Multipart upload of an attachment. |
| POST | /rooms/:room_id/messages/location |
Push lat/lng + label. |
| GET | /rooms/:room_id/files/:file_id/blob |
Download a chat attachment. |
| POST | /rooms/:room_id/read |
Mark up to {ts_ms} as read. |
| GET | /rooms/:room_id/stream |
WebSocket β live message firehose for one room. |
Auto-relay roster (gateway role)
These let an operator manage the rooms this node relays for others.
| Method | Path | Notes |
|---|---|---|
| GET | /relay/rooms |
Rooms this node holds in chatcache. |
| POST | /relay/rooms |
Add a room (body {"room_id"}). |
| DELETE | /relay/rooms/:room_id |
Drop a room from the cache. |
| GET | /relay/rooms/:room_id/stats |
Message count, last activity, byte size. |
Calls
Audio/video calls travel over libp2p but are signalled here. The
/calls/{id}/media WebSocket carries the actual media frames.
| Method | Path | Notes |
|---|---|---|
| GET | /calls |
Ongoing + recent calls. |
| GET | /calls/:call_id |
One call's detail. |
| POST | /calls |
Place a call ({"peer_id","kind":"audio"|"video"}). |
| POST | /calls/:call_id/accept |
Pick up. |
| POST | /calls/:call_id/reject |
Decline. |
| POST | /calls/:call_id/hangup |
End an active call. |
| POST | /calls/:call_id/mute |
{"mic": bool, "cam": bool}. |
| POST | /calls/:call_id/camera |
Switch front/back camera (Android only). |
| POST | /calls/:call_id/remote-mute |
Tell the other side to mute (mgmt-key gated). |
| POST | /calls/:call_id/stats |
Push WebRTC-style stats from the client. |
| GET | /calls/:call_id/media |
WebSocket β opus/h264 frame transport. |
| GET | /calls/events |
SSE β call lifecycle (ring, accept, end). |
| GET | /calls/auto-answer-log |
Recent auto-answered calls (Pi-camera bridge). |
Tunnels, port forwards, system VPN
These three share the underlying multi-hop tunnel substrate; the surface above is different.
Tunnels (HTTP CONNECT proxy)
| Method | Path | Notes |
|---|---|---|
| GET | /tunnels |
Open circuits, with byte counters. |
| POST | /tunnels |
Open one ({"exit_node_id","peer_mgmt_key_id"}). |
| DELETE | /tunnels/:circuit_id |
Tear one down. |
| GET | /tunnels/proxy-info |
The local HTTP CONNECT proxy address (host:port). |
Port forwards (ssh -L / ssh -R style)
| Method | Path | Notes |
|---|---|---|
| GET | /forwards |
Configured forwards + live state (listening, error, byte cnt). |
| POST | /forwards |
Create. Body mirrors port_forwards[] in config.yaml. |
| PATCH | /forwards/:id |
Edit name / listen / remote / route. Persisted to disk. |
| DELETE | /forwards/:id |
Stop and remove. |
| POST | /forwards/:id/start |
Bring up if autostart is false. |
| POST | /forwards/:id/stop |
Pause without deleting. |
Each entry on GET /forwards returns these fields. New flags
shipped with the UPnP rollout:
open_via_upnp(bool, settable on POST/PATCH) β request a router-side port mapping for the listener (this node's router forforward, the peer's forreverse). Best-effort.external_addr(string, read-only) β populated when UPnP succeeded. Forward direction reads it from the localupnp.Manager; reverse reads it from the exit'sTunnelListenAck.external_addr. Empty when the mapping wasn't requested or didn't take.
System VPN
The fd-bound start path is JNI-only (Android VpnService delivers a TUN
fd). The REST surface here covers stop/stats plus the desktop-Windows
start that uses the helper service internally.
| Method | Path | Notes |
|---|---|---|
| POST | /vpn |
Start ({"exit_node_id","peer_mgmt_key_id","exit_name"}). 501 on Linux/macOS. |
| GET | /vpn |
{running, stats}. stats is a free-form human string. |
| DELETE | /vpn |
Stop. |
Management keys
Mgmt keys gate operations that the swarm-PSK-alone shouldn't authorise: inbound tunnels, call auto-answer, remote mute, push-gateway delivery.
| Method | Path | Notes |
|---|---|---|
| GET | /mgmt-keys/self |
"My keys" β what I hand to peers. |
| POST | /mgmt-keys/self |
Mint a key with {caps: ["tunnel","camera"], expires_at_ms}. |
| PATCH | /mgmt-keys/self/:id |
Rename / change caps / extend expiry. |
| DELETE | /mgmt-keys/self/:id |
Revoke. Active circuits drop. |
| GET | /mgmt-keys/peers |
Keys other peers have given me. |
| POST | /mgmt-keys/peers |
Save a peer-issued key (paste from QR). |
| PATCH | /mgmt-keys/peers/:id |
Rename only β caps come from the issuer. |
| DELETE | /mgmt-keys/peers/:id |
Forget. |
| GET | /mgmt-keys/capabilities |
Catalogue of known capability strings (for the UI dropdown). |
S3 admin
The S3 gateway has its own access-key store, independent of the Web UI bearer.
| Method | Path | Notes |
|---|---|---|
| GET | /s3/keys |
List access-key IDs (secret is returned only at creation). |
| POST | /s3/keys |
Mint {access_key_id, secret_access_key}. |
| DELETE | /s3/keys/:key_id |
Revoke. |
| GET | /s3/permissions |
Grants (key_id, bucket, ops). |
| PUT | /s3/permissions |
Set {key_id, bucket, ops: ["read","write","delete"]}. |
| DELETE | /s3/permissions |
Remove a grant (same body shape minus ops). |
The endpoints stay available even when the S3 listener is off, so an
operator can pre-provision keys before flipping node.s3.enabled.
Profile, contacts, settings
| Method | Path | Notes |
|---|---|---|
| GET | /profile |
Display name, avatar, push endpoint, metered status. |
| PUT | /profile |
Update. The hello broadcaster re-emits the changed fields. |
| POST | /profile/push-test |
Send a no-op POST to the saved push endpoint to verify it works. |
| GET | /contacts |
Curated favourites list. |
| POST | /contacts / PUT .../:node_id |
Upsert. |
| GET | /contacts/:node_id |
One contact. |
| DELETE | /contacts/:node_id |
Remove. |
| GET | /settings |
Runtime settings the UI can edit (enrichment API keys, etc.). |
| PUT | /settings |
Update. Enrichment workers restart so new API keys take effect. |
Inbound webhooks
Configured in config.yaml (webhooks: [{token, room_id}, β¦]) β no
runtime CRUD. The endpoint accepts Slack/Mattermost/Discord-shape bodies
and posts to the mapped chat room.
| Method | Path | Notes |
|---|---|---|
| POST | /hooks/:token |
Forwards body into the configured room. Bypasses bearer auth. |
| GET | /hooks/:token |
Probe β returns {ok: true} if the token resolves. |
A missing or unknown token returns 404, not 401 β there's no
information leak about which tokens exist.
Events (SSE)
curl -N -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:8080/api/v1/events/stream
: connected
event: chat_message
data: {"type":"chat_message","room_id":"β¦","sender_node_id":"12Dβ¦",
"ts_ms":1716549300000,"message_type":"TEXT","content_hash":"β¦","is_self":false}
: keepalive
20 s : keepalive comments keep reverse proxies from reaping idle
connections. The wire envelope is the same eventEnvelope shape for
every event type β switch on the type field rather than the SSE
event: line if you only care about the JSON.
Audit
| Method | Path | Notes |
|---|---|---|
| GET | /audit?since=&types= |
JSON list of audit events. Backed by a bounded on-disk ring. |
| GET | /audit/export |
Streamed JSON-Lines for archival. Accepts ?token=. |
AI agents
The full "universal AI-agent" surface β Claude Code, OpenCode, etc. It's the largest single subsystem in the API; consult the dedicated developer scenario for an end-to-end walkthrough.
Instance lifecycle:
| Method | Path | Notes |
|---|---|---|
| GET | /capabilities |
Which agent binaries are installed on this host. |
| GET | /instances |
All agent instances on this node. |
| POST | /instances |
Create ({"type":"claude"|"opencode", "name":"β¦"}). |
| GET | /instances/:id |
Detail. |
| PATCH | /instances/:id |
Rename / change defaults. |
| DELETE | /instances/:id |
Stop and remove the per-instance config dir. |
| POST | /instances/:id/refresh-auth |
Re-probe auth status. |
| POST | /instances/:id/auth-login |
Trigger the browser OAuth flow on the daemon host. |
| GET | /instances/:id/auth-methods |
What's available for this agent type. |
| GET | /instances/:id/providers |
Configured LLM providers. |
| PUT | /instances/:id/providers/:provider |
Set an API key. |
| DELETE | /instances/:id/providers/:provider |
Drop the key. |
| POST | /instances/:id/providers/:provider/oauth/authorize |
Start an OAuth provider flow. |
| POST | /instances/:id/providers/:provider/oauth/callback |
Finish it. |
| GET | /instances/:id/models |
Models the configured providers expose. |
| GET | /instances/:id/mcp |
Configured MCP servers. |
| POST | /instances/:id/mcp |
Add one. |
| DELETE | /instances/:id/mcp/:name |
Remove. |
| POST | /instances/:id/mcp/:name/oauth |
Start MCP-server OAuth. |
| DELETE | /instances/:id/mcp/:name/oauth |
Drop the cached MCP token. |
| POST | /instances/:id/workspaces |
Register a workspace folder for Code-mode. |
| DELETE | /instances/:id/workspaces |
Unregister. |
| GET | /instances/:id/share-url |
Get the meshhold://join/agent/β¦ URL. |
| POST | /instances/:id/reset-key |
Rotate access key (force-kicks every joined device). |
| POST | /instances/remote |
Add an instance hosted on another node by share URL. |
| GET | /instances/:id/search?q=β¦ |
Search transcripts across all sessions. |
| POST | /instances/:id/attachments |
Upload a chat attachment. |
Sessions:
| Method | Path | Notes |
|---|---|---|
| GET | /instances/:id/sessions |
List. |
| POST | /instances/:id/sessions |
Create. |
| GET | /sessions/:sid |
Detail. |
| GET | /sessions/:sid/transcript |
Full message history. |
| GET | /sessions/:sid/events |
SSE β live agent stream-json fan-out. Accepts ?token=. |
| GET | /sessions/:sid/files |
List / download workspace output files. Accepts ?token=. |
| POST | /sessions/:sid/messages |
Send a user turn. |
| POST | /sessions/:sid/stop |
Cancel the current run. |
| POST | /sessions/:sid/archive / .../unarchive |
Hide / unhide. |
| POST | /sessions/:sid/resume |
Re-attach to a previously-paused session. |
| POST | /sessions/:sid/fork |
Branch from a transcript point. |
| DELETE | /sessions/:sid |
Drop entirely. |
| PATCH | /sessions/:sid/settings |
Tweak per-session model / system prompt. |
| GET | /sessions/:sid/approvals |
Pending tool-call approvals. |
| POST | /sessions/:sid/approvals/:aid |
{"decision":"allow"|"deny"}. |
| POST | /sessions/:sid/questions/:qid/reply |
Answer an interactive /question. |
| POST | /sessions/:sid/questions/:qid/reject |
Skip. |
POST /_mcp/sessions/:sid/approve is the loopback hook the daemon-spawned
meshhold _mcp-approve helper calls β not a user-facing endpoint.
Diagnostics
For the Profile page's "Report a problem" button.
| Method | Path | Notes |
|---|---|---|
| POST | /diagnostics/reports |
Build a redacted bundle (config + logs + system state). |
| GET | /diagnostics/reports |
List saved reports. |
| GET | /diagnostics/reports/:id |
Download the .zip. Accepts ?token=. |
| DELETE | /diagnostics/reports/:id |
Remove from disk. |
503 when the daemon has no writable reports dir (headless install without
$HOME).
Status codes
| Code | Meaning |
|---|---|
200 |
OK. |
204 |
Success with no body (logout, mutations that don't echo). |
400 |
Bad request β malformed JSON, missing required field, invalid path. |
401 |
Missing / invalid / expired bearer. |
403 |
Vault locked or operation not permitted by the held mgmt-key caps. |
404 |
Unknown vault / node / room / forward / session / hook token. |
409 |
Conflict β vault name collision, room already exists. |
500 |
Internal error. The body's error field has the message; the daemon log has more. |
501 |
Not implemented on this platform (e.g. POST /vpn on Linux). |
503 |
Daemon in limited mode β swarm key missing or P2P stack down. Retry after setup. |
507 |
Vault hard quota exceeded. Body carries vault_id, hard_quota, would_be_size. |