Sapphire runs a single FastAPI server on port 8073 (HTTPS). Every endpoint below requires authentication — either a browser session or an API key.
Routes are split across modules under core/routes/, plus a few app-level routes in core/api_fastapi.py (login/setup pages, avatar files, plugin web assets). This doc covers the full surface.
Authentication
Browser Session
Log in at /login with your password. Sessions last 30 days.
Named API Tokens (Programmatic Access)
For scripts, external tools, and integrations, mint a named token at Settings > System > API Keys (or POST /api/system/api-tokens) and send it as a Bearer header:
curl -k https://localhost:8073/api/status \
-H "Authorization: Bearer $SAPPHIRE_TOKEN"
The full token value is shown once at creation — copy it then. Listing shows only the last 4 characters; each token is individually revocable (DELETE /api/system/api-tokens/{token_id}).
CSRF
CSRF tokens are required for browser sessions on POST/PUT/DELETE requests. Bearer-token auth needs no CSRF token — no extra headers. The /ws/conversation WebSocket accepts session cookies only (same-origin enforced).
Rate Limiting
5 attempts per 60 seconds per IP on auth endpoints.
Endpoints
Core
| Method |
Endpoint |
Purpose |
| GET |
/api/health |
Health check, open. name when the instance has one, mcp: true when the MCP door is on |
| GET |
/api/status |
Unified UI state (prompt, context, spice, TTS/STT readiness) |
| GET |
/api/init |
Mega initialization (all toolsets, prompts, personas, spices, settings) |
MCP (Model Context Protocol)
| Method |
Endpoint |
Purpose |
| POST |
/mcp |
Streamable HTTP MCP server, tools only, JSON answers, no session. Off until Settings > MCP Server turns it on (404). Bearer API token |
| GET |
/api/mcp/tools |
Every tool with its state for the settings page: {enabled, tools: [{name, description, module, plugin, scoped, exposed, own}]} |
The door speaks initialize, ping, tools/list and tools/call; notifications get 202. It always offers two tools of its own: ask (text, chat, from: a turn in that chat, made if missing, her answer as the result) and tell (the same without waiting). The rest are the tools ticked in settings; tools of plugins that declare scopes are not offered yet. Clients: another Sapphire added as a device, or claude mcp add --transport http sapphire https://host:8073/mcp --header "Authorization: Bearer <token>".
Persona keys. An API token made with a persona (POST /api/system/api-tokens with persona, or the picker under Settings > System > API Keys) is that persona at this door. The client never names a voice or a scope; both come from the persona. These tools are for the client outside and are not in her own toolkit:
| Tool |
What it does |
speak (text) |
Says the words on this machine's speakers in the persona's voice, pitch and speed. Returns when said. Waits for her own voice to end, holds the wake word off, and never changes her voice |
ding |
Plays a short chime on this machine's speakers |
listen (seconds, 3 to 60) |
Offered only when MCP_SERVER_MIC is on. A chime, then this machine's microphone records until the speaker stops; returns Heard: "...". The wake word's stream is closed for the length of it and brought back after. Refused during a voice turn of her own, a live conversation, or a private chat with a cloud transcriber |
memory_save, memory_search, memory_recent, memory_delete |
The persona's own memory scope and no other. Not offered when the persona's memory scope is default, global or unset: no key holds her scope |
tools/mcp-bridge.py is a stdio bridge to this door for clients that will not take a self-signed cert (Claude Code). It reads the token from user/mcp-persona.token (or SAPPHIRE_MCP_TOKEN, or the file named by SAPPHIRE_MCP_TOKEN_FILE, so two clients can each hold their own key), answers the handshake itself while she restarts, and serves the last tool list it saw.
Chat
| Method |
Endpoint |
Purpose |
| POST |
/api/chat |
Send message, get response |
| POST |
/api/chat/stream |
Streaming SSE response — one turn per chat: a second stream while a turn is live on that chat returns 409. Optional chat in the body addresses a chat BY NAME (a room's rail bound to its session): the active chat runs as before; any other chat runs pinned to its own stored settings, never the active pointer. Sealed → 409, missing → 404. continue_from (an assistant turn's timestamp) resumes the chat's last reply in place: no user row is sent or saved, the row is edited on success and untouched on Stop |
| POST |
/api/chat/attach |
Rejoin a turn after losing the feed ({ticket?, chat?, since?, audio?}). Turns are server-owned: the engine runs on its own thread and a viewer leaving never ends it. By ticket (the stream's first line names it): still queued → 202 {state:"queued", position}; live or just finished → replays the ring exact-once after since (every SSE line carries seq, first line {type:"attached", audio}), then follows live; 204 = it ran and is gone; 404 unknown ticket (a restart); 409 sealed chat. audio: true asks to hear her — granted only while nothing has been spoken yet. By chat alone: 204 = no live turn; 404 unknown chat; 409 sealed |
| POST |
/api/cancel |
Cancel active stream (?chat= scopes to one chat) |
| GET |
/api/events |
SSE event stream (real-time UI updates) |
| GET |
/api/history |
Get chat message history (?chat= reads a chat by name; absent = active) |
Chat Sessions
| Method |
Endpoint |
Purpose |
| GET |
/api/chats |
List all chats |
| POST |
/api/chats |
Create new chat |
| GET |
/api/chats/search |
Search chat content (private chats excluded while the vault is sealed) |
| DELETE |
/api/chats/{name} |
Delete chat (also deletes its plugin/playthrough data) |
| POST |
/api/chats/{name}/activate |
Switch active chat |
| GET |
/api/chats/active |
Get active chat name |
| GET |
/api/chats/{name}/settings |
Get chat settings |
| PUT |
/api/chats/{name}/settings |
Update chat settings |
| POST |
/api/chats/{name}/rename |
Rename a chat (carries tool images, message rows, and RAG scope along) |
| POST |
/api/chats/{name}/archive |
Toggle a chat's archived flag (UI shade — chat stays fully functional) |
| GET |
/api/chats/{name}/export |
Full raw export of one chat (name, settings, messages) |
| POST |
/api/chats/bulk-export |
Export many chats as one JSON document |
| POST |
/api/chats/bulk-export-zip |
Export many chats as a zip, one JSON file per chat |
| POST |
/api/chats/bulk-delete |
Delete many chats in one call (per-chat results) |
| POST |
/api/chats/bulk-clear |
Clear messages in many chats (chats survive, histories wiped) |
| POST |
/api/chats/{name}/trim |
Turn-snapped middle trim — keep the first A and last B turns |
| POST |
/api/chats/{name}/repair |
Diagnose/repair a chat's unreadable message rows |
| POST |
/api/chats/{name}/compress |
Start the background compress job (summarize history) |
| GET |
/api/chats/compress/status |
Status of the one-at-a-time compress job (UI polls this) |
| GET |
/api/chats/{name}/prompt-preview |
The exact system prompt + ghost envelope the next turn would send |
Message History
| Method |
Endpoint |
Purpose |
| DELETE |
/api/history/messages |
Remove messages (by count, user message, or clear all with count=-1) |
| POST |
/api/history/messages/remove-last-assistant |
Remove last assistant message |
| POST |
/api/history/messages/remove-from-assistant |
Remove from last assistant message onward |
| DELETE |
/api/history/tool-call/{id} |
Delete specific tool call |
| POST |
/api/history/messages/edit |
Edit a message (the message-edit routes act on the active chat; a rail bound by name sends ?chat= and gets 409 if that chat is no longer active) |
| GET |
/api/history/raw |
Export raw chat history |
| POST |
/api/history/import |
Import chat history |
TTS / STT / Audio
| Method |
Endpoint |
Purpose |
| POST |
/api/tts |
Generate TTS audio |
| POST |
/api/tts/stream |
Streaming TTS — per-chunk OGG over chunked transfer (v2.7.0) |
| POST |
/api/tts/preview |
Preview voice sample |
| GET |
/api/tts/status |
TTS server status |
| POST |
/api/tts/stop |
Stop TTS playback |
| POST |
/api/tts/test |
Test TTS provider connectivity |
| GET |
/api/tts/voices |
List voices for active TTS provider |
| POST |
/api/tts/voices |
List voices (with optional api_key for pre-save browsing) |
| POST |
/api/transcribe |
Transcribe audio file |
| GET |
/api/stt/vad-status |
Silero VAD warmup status |
| POST |
/api/stt/vad-test |
Test mic input against the VAD threshold |
| POST |
/api/mic/active |
Set web mic active state (suppresses wakeword) |
| POST |
/api/upload/image |
Upload image for chat |
| GET |
/api/audio/devices |
List audio devices |
| POST |
/api/audio/test-input |
Test input device |
| POST |
/api/audio/test-output |
Test output device |
Settings
| Method |
Endpoint |
Purpose |
| GET |
/api/settings |
Get all settings |
| GET |
/api/settings/{key} |
Get a single setting |
| PUT |
/api/settings/{key} |
Update a single setting |
| DELETE |
/api/settings/{key} |
Reset a setting to default |
| PUT |
/api/settings/batch |
Batch update multiple settings |
| POST |
/api/settings/reload |
Force reload from disk |
| POST |
/api/settings/reset |
Reset all settings to defaults |
| GET |
/api/settings/help |
Get setting descriptions |
| GET |
/api/settings/help/{key} |
Get help for a specific setting |
| GET |
/api/settings/tiers |
Get hot vs restart-required status |
| GET |
/api/settings/tool-settings |
Get tool-specific settings |
| GET |
/api/settings/chat-defaults |
Get chat default settings |
| PUT |
/api/settings/chat-defaults |
Update chat defaults |
| DELETE |
/api/settings/chat-defaults |
Reset chat defaults to factory |
| GET |
/api/settings/wakeword-models |
List available wakeword models |
Credentials & SOCKS Proxy
| Method |
Endpoint |
Purpose |
| GET |
/api/credentials |
List configured credential keys |
| PUT |
/api/credentials/llm/{provider} |
Set LLM API key |
| DELETE |
/api/credentials/llm/{provider} |
Remove LLM API key |
| GET |
/api/credentials/socks |
Get SOCKS proxy config |
| PUT |
/api/credentials/socks |
Set SOCKS proxy config |
| DELETE |
/api/credentials/socks |
Remove SOCKS proxy config |
| POST |
/api/credentials/socks/test |
Test SOCKS proxy connection |
| GET |
/api/socks/status |
Trust-strip truth: what the proxy env actually covers right now |
LLM Providers
| Method |
Endpoint |
Purpose |
| GET |
/api/llm/providers |
List LLM providers |
| PUT |
/api/llm/providers/{key} |
Update provider config |
| PUT |
/api/llm/fallback-order |
Set LLM fallback order |
| POST |
/api/llm/test/{provider} |
Test LLM connection |
| POST |
/api/llm/test-thinking/{provider} |
Probe whether the provider's 'disable thinking' switch actually works |
| POST |
/api/llm/custom-providers |
Add a custom LLM provider |
| DELETE |
/api/llm/custom-providers/{key} |
Remove a custom LLM provider |
| GET |
/api/llm/custom-providers/{key}/models |
Fetch models from a custom provider |
| GET |
/api/llm/presets |
List LLM provider presets |
Provider Registry (TTS / STT)
| Method |
Endpoint |
Purpose |
| GET |
/api/tts/providers |
List TTS providers (core + plugin) |
| GET |
/api/stt/providers |
List STT providers (core + plugin) |
Embeddings
| Method |
Endpoint |
Purpose |
| POST |
/api/embedding/test |
Test embedding provider |
| GET |
/api/embedding/providers |
List available embedding providers |
| GET |
/api/embedding/integrity |
Check embedding dimension/integrity across stores |
| POST |
/api/embedding/reembed |
Re-embed all stored vectors (after provider/model change) |
| GET |
/api/embedding/reembed/status |
Re-embed progress |
| POST |
/api/embedding/reembed/cancel |
Cancel an in-progress re-embed |
Privacy Vault
Vault state ({exists, unlocked}) rides the top-level vault key on GET /api/status. Wrong passphrase returns 403, never 401. See PRIVACY.md.
| Method |
Endpoint |
Purpose |
| POST |
/api/vault/setup |
Create the vault and unlock it (409 if one exists) |
| POST |
/api/vault/unlock |
Unlock with the passphrase (idempotent) |
| POST |
/api/vault/lock |
Lock now — synchronous; evicts a private active chat first |
| POST |
/api/vault/rekey |
Change the passphrase (needs the current one; lock state preserved) |
| POST |
/api/vault/move |
Move a prompt or piece in/out of the vault |
| POST |
/api/vault/move-batch |
Move many at once ({direction, items:[{kind:'prompt',name}|{kind:'piece',comp_type,key}]}) — one request, per-item receipts, one change event; 409 if the vault is locked |
Chat privacy is per-chat: PUT /api/chats/{name}/settings with private_chat. Membership changes need the vault unlocked (403 otherwise); while sealed, private chats answer as nonexistent on every by-name route.
System Prompt
| Method |
Endpoint |
Purpose |
| GET |
/api/system/status |
System status (detailed) |
| GET |
/api/system/prompt |
Get current system prompt |
| POST |
/api/system/merge-updates |
Merge missing prompts + personas from app updates |
Personas
| Method |
Endpoint |
Purpose |
| GET |
/api/personas |
List all personas |
| GET |
/api/personas/{name} |
Get persona details |
| POST |
/api/personas |
Create persona |
| PUT |
/api/personas/{name} |
Update persona |
| DELETE |
/api/personas/{name} |
Delete persona |
| POST |
/api/personas/{name}/duplicate |
Clone persona |
| POST |
/api/personas/{name}/load |
Activate persona on current chat |
| POST |
/api/personas/from-chat |
Create persona from current chat settings |
| POST |
/api/personas/{name}/avatar |
Upload avatar (max 4MB) |
| DELETE |
/api/personas/{name}/avatar |
Remove avatar |
| GET |
/api/personas/{name}/avatar |
Get avatar image |
| PUT |
/api/personas/default |
Set default persona for new chats |
| DELETE |
/api/personas/default |
Clear default persona |
| GET |
/api/personas/{name}/export.png |
Export persona as a PNG character card (avatar image + full bundle in a sapphire_persona tEXt chunk) |
| POST |
/api/personas/import-card |
Import persona from an uploaded PNG character card (multipart; overwrite_prompt/overwrite_avatar/overwrite_persona flags; JSON bundles go through /api/personas/import) |
| POST |
/api/personas/import |
Import persona from a portable JSON bundle (legacy format) |
Prompts
| Method |
Endpoint |
Purpose |
| GET |
/api/prompts |
List prompts (pack entries with kind != user are omitted; a hidden {name: kind} map rides along for dropdown labels) |
| GET |
/api/prompts/{name} |
Get prompt details |
| PUT |
/api/prompts/{name} |
Create or update prompt |
| DELETE |
/api/prompts/{name} |
Delete prompt |
| POST |
/api/prompts/{name}/load |
Activate prompt on current chat |
| POST |
/api/prompts/reload |
Reload from disk |
| POST |
/api/prompts/reset |
Reset to defaults |
| POST |
/api/prompts/merge |
Merge defaults into current |
| POST |
/api/prompts/reset-chat-defaults |
Reset chat defaults to factory |
| GET |
/api/prompts/components |
List prompt components (hidden pack scaffolding omitted; hidden_keys carries their names for missing-ref checks) |
| PUT |
/api/prompts/components/{type}/{key} |
Save prompt component |
| DELETE |
/api/prompts/components/{type}/{key} |
Delete prompt component |
| GET |
/api/prompts/piece-usage |
Usage index: which prompts use each piece (+ dangling refs, vault flags) |
| POST |
/api/prompts/pieces/rename |
Safe rename: moves a piece in its store and repoints every reference |
| POST |
/api/prompts/pieces/strip-danglers |
Strip all references to missing pieces (409 while vault locked) |
| GET |
/api/prompts/pieces/trash |
List trashed pieces (plaintext + unlocked-vault stores) |
| POST |
/api/prompts/pieces/trash |
Soft-delete pieces into the trash |
| POST |
/api/prompts/pieces/trash/restore |
Restore trashed pieces (never overwrites a live key) |
| POST |
/api/prompts/pieces/trash/purge |
Empty the piece trash (both stores while unlocked) |
| Method |
Endpoint |
Purpose |
| GET |
/api/toolsets |
List toolsets (?filter=sidebar to exclude module-level) |
| GET |
/api/toolsets/current |
Get active toolset |
| POST |
/api/toolsets/{name}/activate |
Activate toolset |
| POST |
/api/toolsets/custom |
Save custom toolset |
| DELETE |
/api/toolsets/{name} |
Delete toolset |
| POST |
/api/toolsets/{name}/emoji |
Set toolset emoji |
| GET |
/api/functions |
List all available functions |
| POST |
/api/functions/enable |
Enable specific functions |
Spices
| Method |
Endpoint |
Purpose |
| GET |
/api/spices |
List all spices |
| POST |
/api/spices |
Add a new spice to a category |
| PUT |
/api/spices/{category}/{index} |
Update a spice |
| DELETE |
/api/spices/{category}/{index} |
Delete a spice |
| POST |
/api/spices/category |
Create spice category |
| PUT |
/api/spices/category/{name} |
Rename spice category |
| DELETE |
/api/spices/category/{name} |
Delete spice category |
| POST |
/api/spices/category/{name}/emoji |
Set category emoji |
| POST |
/api/spices/category/{name}/toggle |
Enable/disable category |
| POST |
/api/spices/reload |
Reload spices from disk |
Spice Sets
| Method |
Endpoint |
Purpose |
| GET |
/api/spice-sets |
List spice sets |
| GET |
/api/spice-sets/current |
Get active spice set |
| POST |
/api/spice-sets/{name}/activate |
Activate spice set |
| POST |
/api/spice-sets/custom |
Save custom spice set |
| DELETE |
/api/spice-sets/{name} |
Delete spice set |
| POST |
/api/spice-sets/{name}/emoji |
Set spice set emoji |
Memory
| Method |
Endpoint |
Purpose |
| GET |
/api/memory/scopes |
List memory scopes |
| POST |
/api/memory/scopes |
Create scope |
| DELETE |
/api/memory/scopes/{name} |
Delete scope |
| GET |
/api/memory/list |
List memories (grouped by label) |
| PUT |
/api/memory/{id} |
Update memory |
| DELETE |
/api/memory/{id} |
Delete memory |
| GET |
/api/memory/export |
Export all memories in scope as JSON |
| POST |
/api/memory/import |
Import memories from JSON |
| GET |
/api/memory/duplicates |
Find near-duplicate memories via vector similarity |
Knowledge
| Method |
Endpoint |
Purpose |
| GET |
/api/knowledge/scopes |
List knowledge scopes |
| POST |
/api/knowledge/scopes |
Create scope |
| DELETE |
/api/knowledge/scopes/{name} |
Delete scope |
| GET |
/api/knowledge/tabs |
List knowledge tabs (in scope) |
| POST |
/api/knowledge/tabs |
Create tab |
| GET |
/api/knowledge/tabs/{id} |
Get tab with entries |
| PUT |
/api/knowledge/tabs/{id} |
Update tab |
| DELETE |
/api/knowledge/tabs/{id} |
Delete tab |
| POST |
/api/knowledge/tabs/{id}/entries |
Add entry |
| POST |
/api/knowledge/tabs/{id}/upload |
Upload file (auto-chunks + embeds) |
| DELETE |
/api/knowledge/tabs/{id}/file/{name} |
Delete uploaded file entries |
| PUT |
/api/knowledge/entries/{id} |
Update entry |
| DELETE |
/api/knowledge/entries/{id} |
Delete entry |
| GET |
/api/knowledge/tabs/{id}/export |
Export knowledge tab as JSON |
| POST |
/api/knowledge/tabs/import |
Import knowledge tab from JSON |
| GET |
/api/knowledge/dedup |
Find near-duplicate knowledge entries |
| DELETE |
/api/knowledge/dedup/resolve |
Resolve/remove a duplicate entry |
People
| Method |
Endpoint |
Purpose |
| GET |
/api/knowledge/people/scopes |
List people scopes |
| POST |
/api/knowledge/people/scopes |
Create scope |
| DELETE |
/api/knowledge/people/scopes/{name} |
Delete scope |
| GET |
/api/knowledge/people |
List people (in scope) |
| POST |
/api/knowledge/people |
Create/update person |
| DELETE |
/api/knowledge/people/{id} |
Delete person |
| POST |
/api/knowledge/people/import-vcf |
Import vCard file |
| GET |
/api/knowledge/people/export |
Export people as JSON |
| POST |
/api/knowledge/people/import |
Import people from JSON |
Goals
| Method |
Endpoint |
Purpose |
| GET |
/api/goals/scopes |
List goal scopes |
| POST |
/api/goals/scopes |
Create scope |
| DELETE |
/api/goals/scopes/{name} |
Delete scope |
| GET |
/api/goals |
List goals (filtered by scope/status) |
| GET |
/api/goals/{id} |
Get a single goal |
| POST |
/api/goals |
Create goal |
| PUT |
/api/goals/{id} |
Update goal |
| POST |
/api/goals/{id}/progress |
Add progress note |
| DELETE |
/api/goals/{id} |
Delete goal |
Per-Chat Documents (RAG)
| Method |
Endpoint |
Purpose |
| POST |
/api/chats/{name}/documents |
Upload document to chat |
| GET |
/api/chats/{name}/documents |
List chat documents |
| DELETE |
/api/chats/{name}/documents/{file} |
Remove document |
Heartbeat (Scheduled Tasks)
| Method |
Endpoint |
Purpose |
| GET |
/api/continuity/tasks |
List scheduled tasks |
| POST |
/api/continuity/tasks |
Create task |
| GET |
/api/continuity/tasks/{id} |
Get task |
| PUT |
/api/continuity/tasks/{id} |
Update task |
| DELETE |
/api/continuity/tasks/{id} |
Delete task |
| POST |
/api/continuity/tasks/{id}/run |
Run task now |
| POST |
/api/continuity/tasks/{id}/cancel |
Stop a task's in-flight run (the ⏹ on a running card). Ends at the next cancel point — between LLM rounds — and drops queued fires behind it; the task stays enabled. Toggling a task off cancels implicitly |
| GET |
/api/continuity/status |
Scheduler status |
| GET |
/api/continuity/activity |
Recent activity log |
| GET |
/api/continuity/timeline |
Upcoming schedule (future only) |
| GET |
/api/continuity/merged-timeline |
Past activity + future schedule with NOW marker |
Daemon Events
| Method |
Endpoint |
Purpose |
| GET |
/api/events/sources |
List daemon event sources from loaded plugins |
| POST |
/api/events/emit/{source} |
Emit a daemon event to trigger matching tasks |
Notifications
| Method |
Endpoint |
Purpose |
| GET |
/api/notifications |
Rows (active first, newest first) + counts; ?level=error,warning&source=piper&unread=true&limit=200 |
| POST |
/api/notifications |
A row from the browser {title, body?, level?, source?, go?, key?} (that tab painted its toast already) |
| POST |
/api/notifications/read |
Mark everything read (opening the page does this) |
| POST |
/api/notifications/{id}/read |
Mark one row read |
| DELETE |
/api/notifications/{id} |
Remove one row; 409 while it is active |
| DELETE |
/api/notifications |
Remove every row that is not active |
Backup
| Method |
Endpoint |
Purpose |
| GET |
/api/backup/list |
List backups |
| POST |
/api/backup/create |
Create backup |
| DELETE |
/api/backup/delete/{name} |
Delete backup |
| GET |
/api/backup/download/{name} |
Download backup archive (tar.gz) |
| GET |
/api/backup/health |
Backup trust at a glance: scheduler liveness, newest backup age, sentinel halt |
| POST |
/api/backup/estimate |
Estimate backup size with the given exclude patterns (per-folder breakdown) |
| POST |
/api/backup/restore |
Restore an existing backup over user/ (validates, decrypts if needed, restarts) |
| POST |
/api/backup/restore-upload |
Restore from an uploaded backup file |
| GET |
/api/backup/restore-result |
Outcome of the last restore, for the post-reboot banner |
| DELETE |
/api/backup/restore-result |
Dismiss the restore-result banner |
Agents
| Method |
Endpoint |
Purpose |
| GET |
/api/agents/status |
List agents (?chat=name to filter) |
| GET |
/api/agents/kinds |
the agent kinds plugins declared (available, cloud, conversational, spawn_schema) |
| GET |
/api/agents/chats |
chats with agents (live / kept) — hidden chats left out |
| GET |
/api/agents/list?chat= |
one chat's agents: live (progress, pending question, transcript tail) + rows; chat-local, hidden = 404 |
| POST |
/api/agents/spawn |
{chat, kind, mission, options} — the privacy gate reads the chat's own settings |
| GET |
/api/agents/{id}/transcript?chat= |
an agent's transcript ring (chat-local) |
| POST |
/api/agents/{id}/answer · /say · /stop |
{chat, value} — resolve its question · a follow-up turn · end it |
| GET |
/api/chat/queue?chat= |
what waits in a chat's inbox (ids, sources, lanes, ages — never text) |
| POST |
/api/chat/queue/drop |
{ticket, chat?} — take a waiting typed turn out of the inbox |
| — |
POST /api/chat/stream on a busy chat |
no longer 409: the body opens with {"type":"queued","ticket","position"} and streams the turn when it is its turn; merged when folded into the turn ahead; queued_dropped when taken out |
| GET |
/api/agents/providers |
List available LLM providers for agents |
| POST |
/api/agents/{id}/dismiss |
Dismiss an agent |
Workspace Runner
| Method |
Endpoint |
Purpose |
| POST |
/api/workspace/run |
Run a command in a workspace project |
| POST |
/api/workspace/stop |
Stop a running workspace process |
| GET |
/api/workspace/status |
Get status of all running workspaces |
Plugins — Listing & Lifecycle
| Method |
Endpoint |
Purpose |
| GET |
/api/webui/plugins |
List all plugins (loaded, enabled, manifest info) |
| PUT |
/api/webui/plugins/toggle/{name} |
Enable/disable a plugin (live load/unload) |
| POST |
/api/plugins/rescan |
Discover newly added plugins without restart |
| POST |
/api/plugins/{name}/reload |
Hot-reload a plugin (unload + load) |
| GET |
/api/plugins/{name}/check-deps |
Check a plugin's pip dependencies |
| GET |
/api/system/extras |
The optional package sets core knows (flash, …): label, note, installed, the install job in flight |
| GET |
/api/system/extras/{name} |
One set's install state (idle/running/done/failed, pip's last lines) — plugin:{plugin} for a plugin's own set |
| POST |
/api/system/extras/{name}/install |
Install a set in the background (core/extras.py); one at a time; refused on hosted installs |
| POST |
/api/plugins/{name}/install-deps |
Install a plugin's declared packages (pip_dependencies and the optional extra) in the background through core's one pip engine; returns the job state, poll GET /api/system/extras/plugin:{name}; the plugin reloads when pip is done. conda/venv only; refused on hosted installs |
| PUT |
/api/plugins/{name}/surfaces |
Set where the plugin's presence injections show up |
| POST |
/api/plugins/{name}/build-env |
Build (or rebuild) a plugin's dedicated conda env in the background |
| GET |
/api/plugins/{name}/env-status |
Env build/readiness status + build log tail (UI polls this) |
| DELETE |
/api/plugins/{name}/env |
Remove a plugin's conda env (services stop; rebuild any time) |
Plugins — Install & Uninstall
| Method |
Endpoint |
Purpose |
| POST |
/api/plugins/install |
Install plugin from GitHub URL or zip upload |
| DELETE |
/api/plugins/{name}/uninstall |
Uninstall user plugin (unload + delete) |
| GET |
/api/plugins/{name}/check-update |
Check for updates from install source |
| POST |
/api/plugins/{name}/revert |
Swap a plugin back to the version retained by its last update |
Store (read-only proxy of sapphireblue.dev catalog)
| Method |
Endpoint |
Purpose |
| GET |
/api/store/status |
Store reachability + cache info |
| GET |
/api/store/categories |
List plugin categories with counts |
| GET |
/api/store/plugins/list |
List/search store plugins (q, category, featured, sort, page, per_page) |
| GET |
/api/store/plugins/{slug} |
Single plugin detail (description, screenshots, version, author) |
| GET |
/api/store/personas/list |
List/search store personas (same query params as plugins) |
| GET |
/api/store/personas/categories |
Persona categories with counts |
| GET |
/api/store/personas/{slug} |
Detail page for one persona |
| POST |
/api/store/personas/{slug}/install |
Download a persona's PNG card and import it (same overwrite flags as import-card) |
Dashboard
| Method |
Endpoint |
Purpose |
| GET |
/api/dashboard/system-info |
Mem usage, thread count, uptime, disk stats, display name |
| GET |
/api/dashboard/component-status |
TTS/STT/wakeword/LLM readiness states |
| GET |
/api/dashboard/widgets |
List active widget panels for the user's dashboard |
| PUT |
/api/dashboard/widgets |
Save the user's panel layout (order, sizes, settings) |
| GET |
/api/dashboard/widgets/available |
List widgets registered by enabled plugins |
Plugin Settings
| Method |
Endpoint |
Purpose |
| GET |
/api/webui/plugins/{name}/settings |
Get plugin settings |
| PUT |
/api/webui/plugins/{name}/settings |
Save plugin settings |
| DELETE |
/api/webui/plugins/{name}/settings |
Reset plugin settings |
| GET |
/api/webui/plugins/config |
Get plugin config metadata |
Apps, Games & Themes
| Method |
Endpoint |
Purpose |
| GET |
/api/apps |
List plugin apps (plugins with an app/ directory) |
| GET |
/api/games |
List games registered via capabilities.games (empty unless game plugins are enabled) |
| GET |
/api/themes |
List all themes (core + plugin manifest themes) |
| GET |
/api/motions |
List motion themes (animation layer) |
Scene Backgrounds
| Method |
Endpoint |
Purpose |
| GET |
/api/backgrounds |
List the scene library ([{name, url, thumb}]) |
| GET |
/api/backgrounds/{name} |
Serve a scene image (?thumb=1 for thumbnail) |
| POST |
/api/backgrounds |
Upload a scene (re-encoded to webp, full + thumb) |
| DELETE |
/api/backgrounds/{name} |
Delete a scene (full + thumb) |
Fonts
| Method |
Endpoint |
Purpose |
| GET |
/api/fonts |
Font registry + downloaded state (Visual tab's Type cards) |
| GET |
/api/fonts/file/{family} |
Serve a downloaded font file |
| POST |
/api/fonts/download |
Fetch a pinned font (SOCKS-aware), verify sha256, install atomically |
Video Guide
| Method |
Endpoint |
Purpose |
| GET |
/api/videos |
Multi-channel video feed for the in-app Video Guide (cached; ?refresh=1 re-fetches) |
Home Assistant Plugin
| Method |
Endpoint |
Purpose |
| GET |
/api/webui/plugins/homeassistant/defaults |
Get HA default settings |
| POST |
/api/webui/plugins/homeassistant/test-connection |
Test HA connection |
| POST |
/api/webui/plugins/homeassistant/test-notify |
Test HA notification |
| PUT |
/api/webui/plugins/homeassistant/token |
Save HA token |
| GET |
/api/webui/plugins/homeassistant/token |
HA token status |
| POST |
/api/webui/plugins/homeassistant/entities |
Fetch HA entities |
Email Plugin
| Method |
Endpoint |
Purpose |
| GET |
/api/webui/plugins/email/credentials |
Get email credentials |
| PUT |
/api/webui/plugins/email/credentials |
Save email credentials |
| DELETE |
/api/webui/plugins/email/credentials |
Remove email credentials |
| POST |
/api/webui/plugins/email/test |
Test email connection |
| GET |
/api/email/accounts |
List email accounts (multi-scope) |
| PUT |
/api/email/accounts/{scope} |
Set email account for scope |
| DELETE |
/api/email/accounts/{scope} |
Remove email account for scope |
| POST |
/api/email/accounts/{scope}/test |
Test email account |
GitHub Plugin
| Method |
Endpoint |
Purpose |
| GET |
/api/github/accounts |
List GitHub accounts (multi-scope) |
| PUT |
/api/github/accounts/{scope} |
Set GitHub account (PAT) for scope |
| DELETE |
/api/github/accounts/{scope} |
Remove GitHub account for scope |
Bitcoin Plugin
| Method |
Endpoint |
Purpose |
| GET |
/api/bitcoin/wallets |
List bitcoin wallets (multi-scope) |
| PUT |
/api/bitcoin/wallets/{scope} |
Set wallet for scope |
| DELETE |
/api/bitcoin/wallets/{scope} |
Remove wallet for scope |
| POST |
/api/bitcoin/wallets/{scope}/check |
Check wallet balance |
| GET |
/api/bitcoin/wallets/{scope}/export |
Export wallet details |
Google Calendar Plugin
| Method |
Endpoint |
Purpose |
| GET |
/api/gcal/accounts |
List Google Calendar accounts (multi-scope) |
| PUT |
/api/gcal/accounts/{scope} |
Set GCal account for scope |
| DELETE |
/api/gcal/accounts/{scope} |
Remove GCal account for scope |
SSH Plugin
| Method |
Endpoint |
Purpose |
| GET |
/api/webui/plugins/ssh/servers |
Get configured SSH servers |
| PUT |
/api/webui/plugins/ssh/servers |
Replace SSH servers list |
| POST |
/api/webui/plugins/ssh/test |
Test SSH connection |
Avatars
| Method |
Endpoint |
Purpose |
| GET |
/api/avatars |
Get avatar paths for user/assistant |
| POST |
/api/avatar/upload |
Upload avatar (max 4MB) |
| GET |
/api/avatar/check/{role} |
Check if avatar exists for role |
| GET |
/api/avatar/{filename} |
Serve avatar file |
Devices
Login required, except the two doors a device opens with its own key.
| Method |
Endpoint |
Purpose |
| GET |
/api/devices |
Every device with its status, and the device types |
| POST |
/api/devices |
Add a device |
| GET |
/api/devices/{id} |
One device: settings, status, what it can do |
| PUT |
/api/devices/{id} |
Change a device. locked is a map {capability: bool} and changes only the capabilities it names |
| GET |
/api/devices/found/{driver} |
What that driver can see right now, for a pick list: {found: [{id, name, kind}]}. ?device=<id> lends a saved device's settings to the look |
| DELETE |
/api/devices/{id} |
Remove a device and its stored secrets |
| POST |
/api/devices/{id}/test |
Check the device now. Answers {status, device}: a device may have just said what it has |
| POST |
/api/devices/{id}/run |
Run one action. Answers {text, ok, images?} |
| POST |
/api/devices/{id}/voice |
Device key. A satellite sends what it heard, as the body with an audio/* Content-Type or as the form file audio. See Satellite Protocol |
| GET |
/api/devices/{id}/events |
Device key. SSE stream of what that satellite's light should show |
Conversation Mode
| Method |
Endpoint |
Purpose |
| WS |
/ws/conversation |
Browser conversation-mode WebSocket — connecting starts the mode, disconnecting ends it (session-cookie auth only) |
| GET |
/api/runtime/true-speech |
Current true-speech (conversation) mode state — ephemeral, for UI load-state |
| PUT |
/api/runtime/true-speech |
Enter/exit true-speech mode (continuous listen, no wakeword) |
Setup Wizard
| Method |
Endpoint |
Purpose |
| GET |
/api/setup/provider-status |
Check STT/TTS provider readiness |
| GET |
/api/setup/check-packages |
Check optional package installation status |
| GET |
/api/setup/wizard-step |
Get current wizard step |
| PUT |
/api/setup/wizard-step |
Set wizard step |
Metrics (Token Usage)
| Method |
Endpoint |
Purpose |
| GET |
/api/metrics/enabled |
Check if metrics tracking is enabled |
| PUT |
/api/metrics/enabled |
Toggle metrics tracking |
| GET |
/api/metrics/summary |
Aggregate token usage summary (?days=30) |
| GET |
/api/metrics/breakdown |
Usage broken down by model (?days=30) |
| GET |
/api/metrics/daily |
Daily usage for charting (?days=30) |
System Updates & Integrity
| Method |
Endpoint |
Purpose |
| GET |
/api/system/update-check |
Check for Sapphire updates |
| POST |
/api/system/update |
Apply update |
| DELETE |
/api/system/update |
Cancel a scheduled update that hasn't applied yet |
| GET |
/api/system/last-update-result |
Result of the most recent deferred update attempt (read-once, then cleared) |
| GET |
/api/system/integrity |
Verify the core install against the shipped manifest (SHA256) |
| POST |
/api/system/integrity/repair |
Restore files that don't match the manifest (git installs), per-file status |
| POST |
/api/system/restart |
Restart Sapphire |
| POST |
/api/system/shutdown |
Shutdown Sapphire |
API Tokens
| Method |
Endpoint |
Purpose |
| GET |
/api/system/api-tokens |
List programmatic API tokens |
| POST |
/api/system/api-tokens |
Create a named API token. Optional persona: the persona it speaks as at the MCP door (400 when there is no such persona) |
| DELETE |
/api/system/api-tokens/{token_id} |
Revoke an API token |
| POST |
/api/system/password |
Change the login password ({current, new}; verifies the current one, 5/min, 10+ chars and ≤72 bytes). The calling tab stays logged in; every other session is logged out. Named bearer tokens are unaffected |
| Method |
Endpoint |
Purpose |
| GET |
/api/tool-image/{id} |
Serve tool-generated image |
Docs
| Method |
Endpoint |
Purpose |
| GET |
/api/docs |
List documentation tree |
| GET |
/api/docs/search |
Search across all docs (?q=query) |
| GET |
/api/docs/{path} |
Get raw markdown content of a doc |
Static Assets & Plugin Web
| Method |
Endpoint |
Purpose |
| GET |
/plugin-web/{name}/{path} |
Serve plugin web/app assets |
| GET |
/workspace/{project}/{path} |
Serve Claude Code workspace files |
| GET |
/cdn-cache/{path} |
Serve locally cached third-party assets |
Reference for AI
Sapphire API reference for programmatic access. Single FastAPI server, https://localhost:8073; every endpoint is auth-gated.
AUTH:
- Browser: session cookie via /login (30-day sessions; CSRF token required on mutations)
- Programmatic:
Authorization: Bearer <token> — named API tokens minted at Settings > System > API Keys or POST /api/system/api-tokens (full value shown ONCE at creation; list shows last-4; revoke via DELETE /api/system/api-tokens/{token_id})
- Bearer auth needs no CSRF token
- /ws/conversation accepts session-cookie auth only (same-origin enforced)
- Rate limit: 5 attempts/60s per IP on auth endpoints
ROUTE MODULES (core/routes/):
- chat.py: chat + stream (SSE; one turn per chat — 409 if a turn is already live), cancel, events SSE, health/status/init, history editing, chat sessions + lifecycle (rename, archive, trim, repair, compress + status, bulk delete/clear/export, single export, prompt-preview)
- content.py: prompts, prompt components, piece trash/rename/usage, toolsets, functions, spices, spice sets, personas (PNG character-card export.png / import-card, legacy JSON import)
- settings.py: settings CRUD, credentials, SOCKS proxy + /api/socks/status, LLM providers + test + test-thinking, custom providers, presets, TTS/STT/embedding provider registries, system status, system prompt (read-only)
- system.py: backup suite (list/create/delete/download tar.gz/health/estimate/restore/restore-upload/restore-result), audio devices, continuity tasks, setup wizard, avatars, restart/shutdown, update + cancel + last-update-result, integrity verify + repair, metrics, api-tokens, daemon events, dashboard system-info + component-status, runtime true-speech GET/PUT
- plugins.py: plugin listing/toggle/rescan/reload/surfaces, install/uninstall/revert/check-update, deps check/install, per-plugin conda envs (build-env, env-status, env DELETE), apps/games/themes/motions, plugin settings, HA/email/bitcoin/gcal/github/ssh routes
- knowledge.py: embedding test/integrity/reembed, memory, goals, knowledge tabs/entries/dedup, people, per-chat RAG documents, export/import
- tts.py: TTS generate/stream/preview/status/stop/test, voices, transcribe, mic active, STT VAD status/test, image upload
- agents.py: agent status/providers/dismiss, workspace run/stop/status
- vault.py: privacy vault setup/unlock/lock/rekey/move
- conversation.py: /ws/conversation WebSocket (browser conversation mode — connect starts, disconnect ends)
- store.py: read-only store proxy — plugins AND personas (status, categories, list/search, detail, persona install)
- dashboard.py: dashboard widget layout + available widgets
- backgrounds.py: scene background library (list/serve/upload/delete, webp)
- fonts.py: font registry, serve, pinned sha256-verified download
- videos.py: in-app Video Guide feed (cached)
- media.py: tool-image serving
- docs.py: doc tree, search, markdown content
- body.py: multi-body runtime (wake, health, events SSE)
Plus app-level routes in core/api_fastapi.py: /login, /logout, /setup, /api/avatar/{filename}, /plugin-web/{name}/{path}, /workspace/{project}/{path}, /cdn-cache/{path}.
KEY ENDPOINTS:
- GET /api/status — unified UI state (prompt, context, spice, streaming, TTS/STT readiness, vault state)
- GET /api/init — mega endpoint (all toolsets, prompts, personas, spices, settings in one call)
- POST /api/chat/stream — SSE streaming chat response (409 on a busy chat)
- GET /api/events — SSE event stream for real-time UI updates
CHAT FLOW:
- POST /api/chat or /api/chat/stream with {"text": "message", "chat": "optional chat name"} —
chat names the target chat; the active chat runs as before, another chat runs pinned to its own stored settings (never the active pointer)
- Response streams as SSE events (content, tool_pending, tool_start, tool_end, reload)
- POST /api/cancel (?chat= to scope) to abort; a second stream on a busy chat WAITS its turn — the body says
queued and closes, the bus says inbox_ticket {ticket, state} when it starts. Turns are server-owned: dropping the SSE socket does NOT cancel — POST /api/chat/attach {ticket, since?} rejoins your turn (every line carries seq; 202 still queued, 204 = it already finished)
- GET /api/history?chat= reads any chat by name; GET /api/chats?kind=game&slim=1 lists game/story sessions with trimmed settings; POST /api/chats {"name", "settings"} stamps settings at birth (private_chat refused there — use the vault flip)
PLUGIN MANAGEMENT:
- POST /api/plugins/install — GitHub URL or zip upload
- DELETE /api/plugins/{name}/uninstall — user plugins only
- POST /api/plugins/{name}/revert — roll back to the version retained by the last update
- POST /api/plugins/{name}/reload — hot-reload
- POST /api/plugins/rescan — discover new plugins
- PUT /api/webui/plugins/toggle/{name} — live enable/disable
MULTI-ACCOUNT CREDENTIALS (email/gcal/github):
- GET /api/{type}/accounts — list all scoped accounts
- PUT /api/{type}/accounts/{scope} — set account for scope
- DELETE /api/{type}/accounts/{scope} — remove
- Bitcoin differs: /api/bitcoin/wallets[/{scope}] (+ /check, /export) — wallets, not accounts
COMMON PATTERNS:
- Scoped endpoints use ?scope=name query param
- File uploads use multipart/form-data
- Toolsets: /api/toolsets (not /api/abilities — legacy name removed)
- Most endpoints return JSON
- 200/201 success, 400 validation, 403 auth/CSRF, 404 not found, 409 conflict (busy chat, existing vault), 503 system not ready
Perception & cadence (Game Room F3, 2026-09-09)
| Method |
Path |
Body / Query |
Notes |
| POST |
/api/perception/{chat} |
{frames: [{data, media_type}], text, source} |
Deposit what she should see next for chat (a game board, an mpv watcher). Latest wins; frames are model-only for the next unprompted turn, never persisted. 404 for an unknown or sealed chat. |
| GET |
/api/cadence/{chat} |
— |
The cadence organ's record for chat: armed, mode (event\|timer), paused, next_in, last_at, skips, pending, fired, every, pokes, ttl_in (every/pokes: event mode fires on every Nth poke; the count since the last one that became her turn). Arming is the room host plugin's business (/api/plugin/game-room/room/cadence/*). |