Base URL: http://localhost:8099
GET /healthServer availability check.
Response 200
{
"status": "ok",
"embed": {
"status": "ok",
"model": "nomic-embed-text",
"dimensions": 768
}
}
GET /health/embedEmbedding model health check. Returns the first vector of a test string to verify the embed backend is responsive.
Response 200
{
"status": "ok",
"model": "nomic-embed-text",
"dimensions": 768
}
Full auth documentation: docs/auth.md. API token docs: docs/api_tokens.md.
Set NAVI_AUTH_ENABLED=false to disable all auth checks. In that mode every request is treated as the local anonymous admin user and OAuth endpoints return 503 if called.
GET /auth/loginRedirect to gnexus-auth OAuth authorization endpoint. Sets PKCE + state internally.
Query params
return_to — URL to redirect back to after login (default: /)platform — browser (default) or android (affects redirect after callback)Response 302 → Location: gnexus-auth /oauth/authorize
GET /auth/callbackOAuth callback. Validates state, exchanges code for tokens, creates DB session.
Query params
code — authorization code from gnexus-authstate — state parameterResponse 302
/ (with Set-Cookie)/auth/mobile-done?sid=<session_id>Errors
400 — invalid state, PKCE failure, or token exchange failed503 — OAuth is not configured (missing gnexus_auth_client_id or gnexus_auth_client_secret)GET /auth/mobile-doneBridge page for Android OAuth. Renders HTML that attempts an automatic deep-link back into the native app via Chrome Intent URL (intent://...), and falls back to a manual button for browsers that block automatic navigation to custom schemes (e.g. DuckDuckGo). Styled with the gnexus UI kit design system.
Query params
sid — session id that will become the navi_auth_session cookieResponse 200 — HTML page
POST /auth/logoutLogout current user. Deletes DB session and clears cookie.
Response 200
{ "ok": true }
GET /auth/meReturn current authenticated user.
Response 200
{
"id": "user-uuid",
"email": "user@example.com",
"display_name": "User Name",
"username": "username",
"first_name": "First",
"last_name": "Last",
"phone": "+1234567890",
"birth_date": "1990-01-01",
"country": "US",
"city": "New York",
"locale": "en-US",
"avatar_url": "https://...",
"profile_url": "https://...",
"role": "admin",
"permissions": ["navi.sessions.read_all", "navi.memory.read_all"]
}
Errors
401 — not authenticatedGET /auth/statusReturn whether auth is enabled on the backend and whether OAuth is configured.
Response 200
{
"enabled": true,
"configured": true
}
enabled — NAVI_AUTH_ENABLED value. When false, the server is in no-auth mode and every request is treated as the anonymous admin user.configured — true if GNAUTH_CLIENT_ID and GNAUTH_CLIENT_SECRET are both set. Always false when enabled is false.GET /agents/profilesList available agent profiles. Non-admin users do not see is_admin_only profiles — the filter is the role gate described in auth.md, so the list differs by role rather than merely being trimmed of secrets.
Order is the profile load order (directory name). The webclient takes the first entry as the default for a new session, so for a user that is the first of the restricted profiles.
Response 200
[
{
"id": "assistant",
"name": "Assistant",
"description": "Everyday assistant for a regular user",
"llm_backend": "ollama",
"model": ["glm-5.3-flash:cloud", "gemma4:31b-cloud"],
"temperature": 0.45,
"top_k": null,
"top_p": null,
"max_iterations": 10,
"iteration_budget_enabled": true,
"think_enabled": true,
"subagent_think_enabled": null,
"tools": {
"agent": {
"native": ["todo", "scratchpad", "filesystem"],
"mcp": {
"navi-web": ["search"]
}
},
"subagent": {
"native": ["todo", "filesystem"],
"mcp": {}
}
}
}
]
GET /agents/toolsList all registered tools (built-in + user tools).
Response 200
[
{
"name": "mcp__navi-web__web_search",
"description": "Search the web using DuckDuckGo.",
"parameters": {"type": "object", "properties": {...}, "required": [...]}
},
{
"name": "filesystem",
"description": "Read, write and list files.",
"parameters": {"type": "object", "properties": {...}, "required": [...]}
}
]
GET /agents/promptsReturn the fully resolved system prompt for each profile (persona + profile system_prompt + context provider injections + one line per MCP server).
Response 200
{
"secretary": "system prompt text...",
"server_admin": "system prompt text..."
}
GET /agents/mcp_serversReturn all configured MCP servers with their resolved tools per profile.
Response 200
{
"navi-web": {
"connected": true,
"tools": [
{"name": "mcp__navi-web__web_search", "description": "..."},
{"name": "mcp__navi-web__web_view", "description": "..."},
{"name": "mcp__navi-web__http_request", "description": "..."}
],
"instructions": "MANDATORY: Before answering ANY question..."
}
}
POST /sessionsCreate a new session.
Auth: requires authenticated user.
Request body
{ "profile_id": "secretary" }
Response 201
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"profile_id": "secretary",
"created_at": "2026-04-10T18:00:00+00:00"
}
Errors
401 — not authenticated403 — the profile is is_admin_only and the caller's role is not admin ({"detail": "Profile '<id>' requires admin access"})404 — profile not foundGET /sessionsList all sessions sorted by activity (pinned first).
Auth: requires authenticated user.
Query params | Param | Default | Description | |---|---|---| | limit | 50 | Page size | | offset | 0 | Items to skip | | profile_id | — | Filter by profile | | search | — | Full-text search over names/previews | | special | — | Session type filter. Omitted — no filtering (all sessions). false — service (special=True) sessions excluded, the webclient default. true — service sessions only |
Response 200 (when pagination params provided)
{
"items": [
{
"session_id": "550e8400-...",
"profile_id": "secretary",
"name": "Research task",
"message_count": 12,
"preview": "Last 60 chars of the most recent message",
"pinned": false,
"created_at": "2026-04-10T15:00:00+00:00",
"last_active": "2026-04-10T18:00:00+00:00"
}
],
"limit": 50,
"offset": 0,
"has_more": true,
"next_offset": 50
}
Response 200 (plain list when no pagination params)
[
{
"session_id": "550e8400-...",
"profile_id": "secretary",
"name": "Research task",
"message_count": 12,
"preview": "Last 60 chars of the most recent message",
"pinned": false,
"created_at": "2026-04-10T15:00:00+00:00",
"last_active": "2026-04-10T18:00:00+00:00"
}
]
name is null until POST /sessions/{id}/generate-name is called.
GET /sessions/{session_id}Full session with message history (display history — never compressed).
Auth: requires authenticated user (or ownership of the session).
Response 200
{
"session_id": "550e8400-...",
"profile_id": "secretary",
"name": "Research task",
"context_token_count": 4913,
"max_context_tokens": 65536,
"created_at": "...",
"last_active": "...",
"messages": [
{
"role": "user",
"content": "Hello",
"created_at": "2026-04-10T18:00:00+00:00"
},
{
"role": "assistant",
"content": "Hi. How can I help?",
"created_at": "2026-04-10T18:00:05+00:00"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "abc123",
"name": "mcp__navi-web__web_search",
"arguments": { "query": "..." }
}
]
},
{
"role": "tool",
"content": "tool result",
"tool_call_id": "abc123",
"name": "mcp__navi-web__web_search"
}
]
}
Message fields (role is always present, others by availability):
| Field | Type | Description | |||
|---|---|---|---|---|---|
role |
`user\ | assistant\ | tool\ | system` | Message author |
content |
`string\ | null` | Text content | ||
images |
string[] |
Base64 images (user/assistant) | |||
tool_calls |
ToolCall[] |
Tool invocations (assistant) | |||
tool_call_id |
string |
ID of the call this result belongs to (tool) | |||
name |
string |
Tool name (tool messages) | |||
thinking |
`string\ | null` | LLM reasoning captured during a tool-calling turn | ||
is_plan |
bool |
Planning phase output — rendered as a plan card, not text | |||
is_compression |
bool |
Marker injected when context compression ran | |||
is_summary |
bool |
Summary message replacing compressed history | |||
created_at |
string (ISO 8601) |
Creation time | |||
elapsed_seconds |
`number\ | null` | Time to complete the turn (final assistant message) | ||
tool_call_count |
`number\ | null` | Number of tool calls in the turn | ||
token_count |
`number\ | null` | Tokens used in the turn |
Errors
404 — session not foundGET /sessions/{session_id}/recallGet the pending recall for a session, if any. Returns the first pending recall only.
Response 200
{
"id": "r1",
"call_type": "once",
"trigger_at": "2026-05-16T14:00:00+00:00",
"interval_seconds": null,
"internal_comment": "Check build logs",
"additional_context_message": "Read /tmp/build.log...",
"status": "pending"
}
Response 200 (no pending recall)
{ "recall": null }
Errors
404 — session not foundDELETE /sessions/{session_id}/recallCancel the pending recall for this session.
Response 200
{ "ok": true }
Errors
404 — session not foundPOST /sessions/{session_id}/recall/skipSkip the next occurrence of a recurring recall (advances trigger_at by interval_seconds).
Response 200
{ "ok": true }
Errors
404 — session not found400 — no recurring pending recallDELETE /sessions/{session_id}Delete a session and its files.
Response 204 — no body
Errors
404 — session not foundPATCH /sessions/{session_id}/pinPin or unpin a session.
Request body
{ "pinned": true }
Response 200
{ "session_id": "...", "pinned": true }
POST /sessions/{session_id}/generate-nameGenerate a short display name for a session from its message history. Called automatically by the client after the first exchange. No-op if the session already has a name.
Response 200
{ "name": "Web search for recipes" }
Returns {"name": null} if there are no user messages yet.
Errors
404 — session not foundGET /sessions/{session_id}/contextLLM context (what the model actually sees). May differ from messages — compressed history replaces old turns with a summary. Debug endpoint.
Auth: requires admin role.
Response 200
{
"session_id": "...",
"profile_id": "secretary",
"message_count": 8,
"total_chars": 4200,
"context": [ ...same format as messages... ]
}
Errors
403 — not adminGET /sessions/{session_id}/planningAll planning phase debug logs for the session. Each entry is one planning run.
Auth: requires admin role.
Response 200
{ "session_id": "...", "logs": [ { "phase": "...", "output": "..." }, ... ] }
Errors
403 — not adminGET /sessions/{session_id}/tasksList the session's background tasks (TaskManager jobs). task_update WS events are not replayed on reconnect, so clients fetch this snapshot to rebuild their backgrounds view after a reload.
Response 200
{
"session_id": "...",
"tasks": [
{
"task_id": "bt-ab12cd34",
"tool": "terminal",
"status": "running",
"args_summary": "sleep 60",
"parent_tool_call_id": "tc-1",
"started_at": "2026-04-10T18:00:00+00:00",
"finished_at": null,
"subagent_tokens": null,
"result_preview": ""
}
]
}
Errors
404 — session not foundGET /sessions/{session_id}/contentList published session content (artifacts registered via content_publish tool).
Response 200
{
"content": [
{
"id": "...",
"filename": "report.html",
"path": "/abs/path/to/content/...",
"size": 102400,
"content_type": "text/html",
"created_at": "2026-04-10T18:00:00+00:00"
}
]
}
Errors
404 — session not foundPOST /sessions/{session_id}/filesUpload a file for a session. Call before sending a message to attach the file.
Request: multipart/form-data, field file.
Limits
.exe, .dll, .so, .sh, .bat, .cmd, .ps1, .vbs, .bin, .elf, and other executable formatsResponse 201
{
"name": "report.pdf",
"size": 102400,
"path": "/abs/path/to/session_files/550e8400-.../report.pdf",
"content_type": "application/pdf"
}
Errors
400 — forbidden extension404 — session not found413 — file exceeds limitGET /sessions/{session_id}/files/{filename}Download or view an uploaded file. Images, PDFs, plain text and HTML are served inline; everything else as an attachment.
Query params
download — force attachment download regardless of content typeResponse 200 — file bytes
Errors
403 — path traversal attempt404 — session or file not foundGET /sessions/{session_id}/filesList all files and directories in the session's file directory (recursive, depth 10).
Response 200
{ "session_id": "...", "files": [{"name": "...", "size": N, "is_dir": false, "path": "..."}, ...] }
Errors — 404 session not found; 403 no access.
GET /sessions/{session_id}/todosReturn the session's current todo list (the parent agent's plan). Sub-agent todos live in isolated KV rows and are not exposed here. Used by the TUI to seed the side panel on attach/switch.
Response 200
{ "session_id": "...", "tasks": [{"index": 0, "text": "...", "status": "pending", "validation": "..."}, ...] }
Errors — 404 session not found; 403 no access.
GET /sessions/{session_id}/messages/archiveReturn older archived messages for scroll-up (lazy history loading), paginated by sequence_number.
Query params — before_seq (int, optional — load messages older than this sequence number), limit (int, default 50, 1–200).
Response 200
{ "items": [<Message>, ...], "has_more": true, "next_before_seq": 123 }
Errors — 404 session not found; 403 no access.
POST /sessions/{session_id}/stopCooperatively stop an in-progress generation for the session. Sent via fetch() (not over the WebSocket) to avoid corrupting the WebSocket receive state.
Response 200
{ "ok": true } // a run was active and signalled to stop
{ "ok": false, "reason": "no active run" }
POST /sessions/{session_id}/messagesSend a message and receive a response synchronously (no streaming). Blocks until the full agent loop completes.
Request body
{ "content": "How many stars are in the galaxy?" }
Response 200
{ "role": "assistant", "content": "Estimates range from 100 to 400 billion." }
Errors
404 — session not found500 — agent error or iteration limit exceededFor production clients prefer WebSocket — it provides streaming, tool progress, and model reasoning.
Headless client authentication. See docs/api_tokens.md for full details.
POST /api-tokensCreate a new API token. Plain token is returned only in this response.
Auth: requires authenticated user.
Request
{ "name": "Smart Watch" }
Response 200
{
"id": 1,
"name": "Smart Watch",
"token": "nav_aB3xYz9WqLmNpQrStUvXyZaBCdEfGhIjKlMnOpQrStUvX",
"token_prefix": "nav_aB3xYz9W…",
"created_at": "2026-05-24T10:00:00+00:00",
"last_used_at": null
}
GET /api-tokensList active (non-revoked) tokens for the current user. Does not expose plain tokens.
Auth: requires authenticated user.
Response 200
{
"items": [
{
"id": 1,
"name": "Smart Watch",
"token_prefix": "nav_aB3xYz9W…",
"created_at": "2026-05-24T10:00:00+00:00",
"last_used_at": "2026-05-24T12:00:00+00:00"
}
]
}
DELETE /api-tokens/{token_id}Revoke (soft-delete) a token belonging to the current user.
Auth: requires authenticated user.
Response 204 — no body
Errors
404 — token not found or does not belong to userWS /ws/sessions/{session_id}Main channel for real-time agent interaction. Supports text streaming, thinking streaming, tool events, file and image attachment.
Connect: if the session is not found, the server closes with code 4004.
On connect: the server immediately sends session_sync (no active run) or starts the reconnect replay flow (run in progress).
All client messages are JSON objects.
{
"type": "message",
"content": "Message text",
"images": ["base64string...", "..."],
"files": [
{ "name": "report.pdf", "size": 102400, "path": "session_files/.../report.pdf" }
]
}
| Field | Required | Description |
|---|---|---|
type |
yes | "message" (a user turn) or "compact" (force context compression now — server streams compression_started → context_compressed, no stream_start; rejected if a run is active) |
content |
yes | Message text (non-empty), required for "message" |
images |
no | Base64 image list. Max 8 images, 50 MB total payload. Both raw base64 and data:image/...;base64,... are accepted — server strips the prefix |
files |
no | Files uploaded via POST /sessions/{id}/files. Server appends their paths to the message content |
Events arrive in the order they are emitted.
stream_start{ "type": "stream_start" }
Processing started. Client should block input.
thinking_delta{ "type": "thinking_delta", "delta": "reasoning fragment..." }
Streaming chunk of model reasoning. Accumulate until thinking_end.
thinking_end{ "type": "thinking_end" }
Reasoning phase complete. Next will be stream_delta or tool calls.
turn_thinking{
"type": "turn_thinking",
"thinking": "full reasoning text...",
"is_subagent": false
}
Complete reasoning block from a tool-calling turn. Not streamed — arrives whole. is_subagent: true means this reasoning came from a subagent inside spawn_agent.
planning_status{
"type": "planning_status",
"phase": "analysis",
"label": "Analysing request...",
"is_subagent": false
}
Progress update during the planning phase. phase is one of analysis, reflect, plan. is_subagent: true — route into the spawn_agent card, not the top-level UI.
plan_ready{
"type": "plan_ready",
"plan": "1. Step one\n2. Step two\n...",
"is_subagent": false
}
Planning complete — full step list. Rendered as a collapsible plan card. is_subagent: true — route into the spawn_agent card.
tool_started{
"type": "tool_started",
"tool": "mcp__navi-web__web_search",
"args": { "query": "weather in moscow" },
"is_subagent": false
}
Agent started executing a tool. Arrives before execution completes — show a spinner. is_subagent: true — call from a subagent.
tool_call{
"type": "tool_call",
"tool": "mcp__navi-web__web_search",
"args": { "query": "weather in moscow" },
"result": "Today +12°C, cloudy.",
"success": true,
"is_subagent": false
}
Tool finished. Arrives after tool_started with the same tool and args. success: false — tool returned an error.
stream_delta{ "type": "stream_delta", "delta": "response fragment..." }
Streaming chunk of the final text response. Accumulate into a string.
stream_end{
"type": "stream_end",
"content": "full response text",
"context_tokens": 4913,
"max_context_tokens": 65536,
"elapsed_seconds": 12.4,
"tool_call_count": 3,
"token_count": 1842
}
Agent finished. content is the full accumulated text (duplicates the sum of stream_delta). Client should unblock input.
stream_stopped{ "type": "stream_stopped" }
Generation was stopped by POST /sessions/{id}/stop.
profile_switched{
"type": "profile_switched",
"profile_id": "server_admin",
"profile_name": "Server Administrator"
}
Agent switched profile via switch_profile tool. New profile takes effect on the next user message. Client should update the profile indicator. Arrives during the stream — before tool_call for switch_profile.
context_compressed{
"type": "context_compressed",
"messages_before": 42,
"messages_after": 12,
"summary": "User asked about..."
}
Context was automatically compressed (triggers at ≥90% of OLLAMA_NUM_CTX, or on demand via {"type":"compact"}). summary is the produced summary text. Informational.
heartbeat{ "type": "heartbeat" }
Keepalive ping sent every 20 s during long silent operations. Client can ignore.
recall_update{
"type": "recall_update",
"session_id": "550e8400-...",
"recall_id": "r1",
"call_type": "once",
"trigger_at": "2026-05-16T14:00:00+00:00",
"status": "pending",
"action": "scheduled"
}
Recall state changed. Sent when a recall is scheduled, cancelled, skipped, fired, or rescheduled. Client should refresh the recall banner via GET /sessions/{id}/recall.
Fields | Field | Type | Description | |-------|------|-------------| | session_id | string | Affected session | | recall_id | string\|null | Recall ID (null for cancel without ID) | | call_type | string\|null | once, recurring, immediate | | trigger_at | string\|null | Next trigger time (ISO 8601) | | status | string\|null | pending, fired, cancelled | | action | string\|null | scheduled, cancelled, skipped, fired, rescheduled |
model_info{ "type": "model_info", "model": "gemma4:31b-cloud" }
Emitted once per turn after the backend resolves a model (may differ from the profile's configured model when fallback picked another). Additive.
todo_updated{ "type": "todo_updated", "session_id": "...", "tasks": [{"index": 0, "text": "...", "status": "in_progress", "validation": "..."}] }
Session todo list changed (auto-populated from the plan, or via the todo tool). Sub-agent todos are NOT emitted here. Additive.
compression_started{ "type": "compression_started", "context_tokens": 46000, "max_context_tokens": 65536 }
Emitted immediately before context compression begins — client can show a spinner.
task_update{
"type": "task_update",
"task_id": "bt-1a2b3c4d",
"session_id": "...",
"tool": "terminal",
"status": "running",
"result_preview": "",
"parent_tool_call_id": "",
"started_at": "2026-04-30T10:00:00+00:00",
"finished_at": null,
"subagent_tokens": null
}
A background task (a tool call detached with background: true, see docs/tasks.md) changed state. status is running | completed | failed | cancelled. Out-of-band: NOT part of the run's replay buffer — a reconnecting client learns task state via the tasks tool/notes, not replay. parent_tool_call_id (when non-empty) binds the update to the tool card that started the task, so the client can attach sub-agent progress to it.
message_queued{ "type": "message_queued", "position": 1, "queue_len": 1, "max": 5, "dropped_total": 0 }
The user's message arrived while a run was active and was queued instead of erroring. It will execute after the current run finishes. queue_len counts queued messages, max is MESSAGE_QUEUE_MAX, dropped_total grows when the queue was full and the oldest message was dropped.
session_sync{ "type": "session_sync", "session_id": "...", "profile_id": "..." }
Client must reload session history from GET /sessions/{id}. Carries the active session_id and profile_id. Sent:
replay_start{ "type": "replay_start", "count": 14 }
About to replay count buffered events from a mid-stream reconnect. Client should suppress cursor animations and in-progress effects during replay.
replay_end{ "type": "replay_end" }
Replay complete. Live events will follow.
error{ "type": "error", "message": "Session not found" }
Processing error. Stream may or may not continue after this.
Simple question, no tools:
stream_start thinking_delta × N (if model has thinking enabled) thinking_end stream_delta × N stream_end
Request with tool calls:
stream_start turn_thinking (reasoning before tool selection, if any) tool_started tool_call turn_thinking (before next tool, if any) tool_started tool_call thinking_delta × N (final response reasoning) thinking_end stream_delta × N stream_end context_compressed (optional, if context was near full)
Request with planning enabled:
stream_start planning_status (phase: analysis) planning_status (phase: plan) plan_ready turn_thinking tool_started tool_call ... stream_end
Request with subagent (spawn_agent):
stream_start tool_started (spawn_agent, is_subagent=false) turn_thinking (is_subagent=true) planning_status (is_subagent=true, if subagent has planning) plan_ready (is_subagent=true, if subagent has planning) tool_started (subagent tool, is_subagent=true) tool_call (is_subagent=true) tool_call (spawn_agent done, is_subagent=false) stream_delta × N stream_end
Reconnect mid-stream:
stream_start
replay_start {"count": N}
ev_0 ... ev_N-1 (buffered events replayed verbatim)
replay_end
(live events continue)
...
stream_end
session_sync
Profile switch (switch_profile):
stream_start tool_started (switch_profile) profile_switched (client updates UI here — before tool_call) tool_call (switch_profile done) stream_delta × N stream_end
All admin endpoints require admin role or specific permissions.
GET /admin/sessionsAll sessions across all users. Supports pagination, search, and sorting.
Query params | Param | Default | Description | |---|---|---| | limit | 50 | Page size | | offset | 0 | Items to skip | | search | — | Filter by session_id, name, user_id or profile_id (case-insensitive) | | sort_by | last_active | last_active, created_at, name, profile_id, user_id, pinned | | sort_order | desc | asc or desc |
Response 200
{
"total": 128,
"limit": 50,
"offset": 0,
"items": [
{
"session_id": "...",
"profile_id": "secretary",
"user_id": "user-uuid",
"name": "Research task",
"message_count": 12,
"pinned": false,
"created_at": "2026-05-04T10:00:00+00:00",
"last_active": "2026-05-04T10:30:00+00:00"
}
]
}
GET /admin/usersAll registered navi_users.
Response 200
[
{
"id": "user-uuid",
"email": "user@example.com",
"display_name": "User Name",
"role": "admin",
"permissions": ["navi.sessions.read_all"],
"created_at": "...",
"updated_at": "..."
}
]
GET /admin/memoryAll memory facts (global view). Requires navi.memory.read_all. Supports pagination, search, and sorting.
Query params | Param | Default | Description | |---|---|---| | limit | 50 | Page size | | offset | 0 | Items to skip | | search | — | Filter by key, value or category (case-insensitive) | | sort_by | updated_at | updated_at, category, key, confidence, source | | sort_order | desc | asc or desc | | user_id | — | If set, return only facts for this user instead of global view |
Response 200
{
"total": 128,
"limit": 50,
"offset": 0,
"items": [
{
"id": "...",
"category": "profile",
"key": "name",
"value": "Eugene",
"source": "conversation",
"confidence": 90,
"updated_at": "2026-05-04T10:00:00+00:00"
}
]
}
GET /admin/profilesAll profiles including admin-only ones. Requires navi.profiles.manage.
GET /admin/sessions/{session_id}Full session details including messages. Bypasses ownership check.
Response 200
{
"session_id": "...",
"profile_id": "secretary",
"user_id": "user-uuid",
"name": "Research task",
"messages": [...],
"context_token_count": 4913,
"max_context_tokens": 65536,
"pinned": false,
"created_at": "...",
"last_active": "..."
}
Errors
404 — session not foundDELETE /admin/sessions/{session_id}Delete any session (bypasses ownership). Also deletes session files.
Response 204 — no body
Errors
404 — session not foundGET /admin/users/{user_id}Single user details.
Response 200
{
"id": "user-uuid",
"email": "user@example.com",
"display_name": "User Name",
"role": "admin",
"permissions": ["navi.sessions.read_all"],
"created_at": "...",
"updated_at": "..."
}
Errors
404 — user not foundGET /admin/users/{user_id}/sessionsSessions owned by a specific user.
Response 200
[
{
"session_id": "...",
"profile_id": "secretary",
"name": "Research task",
"message_count": 12,
"pinned": false,
"created_at": "...",
"last_active": "..."
}
]
POST /admin/ollama/clear-blacklistsManually clear dead-server and dead-model blacklists for the Ollama fallback backend. Useful when a transient failure caused a 5-minute blacklist and you want immediate recovery.
Response 204 — no body
GET /admin/profiles/{profile_id}Full profile configuration including system prompt.
Response 200
{
"id": "secretary",
"name": "Personal Secretary",
"description": "General-purpose assistant",
"short_description": "...",
"full_description": {"specialization": "...", "when_to_use": "...", "key_tools": [...]},
"system_prompt": "...",
"subagent_system_prompt": "...",
"llm_backend": "ollama",
"model": ["gemma4:31b-cloud", "gemma4:26b-a4b-it-q4_K_M"],
"temperature": 0.65,
"top_k": null,
"top_p": null,
"num_thread": null,
"max_iterations": 10,
"planning_phase1_enabled": true,
"planning_phase3_enabled": true,
"think_enabled": true,
"iteration_budget_enabled": true,
"goal_anchoring_enabled": true,
"goal_anchoring_interval": 5,
"anti_stall_enabled": true,
"anti_stall_threshold": 8,
"step_validation_enabled": false,
"tools": {
"agent": {
"native": ["todo", "scratchpad", "filesystem"],
"mcp": {
"navi-web": ["search"]
}
},
"subagent": {
"native": ["todo", "filesystem"],
"mcp": {}
}
},
"context_providers": [],
"is_admin_only": false
}
Errors
404 — profile not foundPUT /admin/profiles/{profile_id}Update profile configuration on disk and in-memory. Accepts partial updates — only provided fields are modified.
Writes config.json back through save_profile_to_dir, so a field the caller did not mention keeps its current value including is_admin_only, which the writer emits alongside is_hidden and is_subagent_only. Both this endpoint and the availability toggle drop the cached system prompts afterwards — every profile's prompt embeds the "Available profiles" block, so a change to one profile's availability changes all of them.
Request body (partial)
{
"temperature": 0.5,
"max_iterations": 20,
"think_enabled": true
}
Response 200
{ "ok": true }
Errors
400 — invalid profile data404 — profile not foundPATCH /admin/users/{user_id}/roleUpdate cached role. Requires admin.
Request body
{ "role": "admin" }
Response 200
{ "ok": true }
PATCH /admin/profiles/{profile_id}/availabilityToggle is_admin_only. Requires navi.profiles.manage.
Writes a row into profile_overrides, which is applied on top of config.json at every startup — so this toggle outlives a restart and wins over the value committed in the file. config.json remains the baseline for any profile with no row. Takes effect immediately for profile lists and for the injected "Available profiles" block.
Request body
{ "is_admin_only": true }
Response 200
{ "ok": true }
All require admin role.
| Endpoint | Purpose |
|---|---|
GET /admin/mcp/config |
Bulk read all MCP server configs (mcp_servers.d/). |
PUT /admin/mcp/config |
Bulk write all MCP server configs. |
GET /admin/mcp/config/{server_name} |
Read one server config. |
PUT /admin/mcp/config/{server_name} |
Create/update one server config. |
DELETE /admin/mcp/config/{server_name} |
Delete one server config. |
POST /admin/mcp/{server_name}/reconnect |
Drop old client, unregister tools, connect fresh, re-register. |
GET /admin/mcp/status |
List servers with connection status and exposed tools. |
POST /admin/mcp/test |
Execute a single MCP tool call in isolation for diagnostics. |
GET /admin/profiles/{profile_id}/mcp · PUT /admin/profiles/{profile_id}/mcpRead/write a profile's tools.agent.mcp / tools.subagent.mcp mapping (which MCP groups the profile exposes). Requires navi.profiles.manage.
GET /admin/recallsList all scheduled recalls with pagination/filtering (admin view across all users/sessions).
POST /webhooks/gnexus-authReceive webhooks from gnexus-auth. Verified via HMAC.
Response 200
{ "ok": true }
Errors
400 — invalid payload503 — OAuth not configuredPOST /webhooks/synapse (also /webhooks/synapse/)Receive one GNEXUS Synapse s2s delivery (see synapse.md). Signature verified against every active per-user target secret; replays are re-acked but never acted on twice. A non-duplicate delivery schedules a fire-and-forget agent reaction (per-user reactions_enabled gate).
Response 200
{ "received": true, "event_id": "evt-..." }
Errors
401 — invalid webhook signature (or unparseable body)503 — no navi-side Synapse targets configuredPer-user reaction settings (auth required). See details in synapse.md.
GET /synapse-settingsReturns the caller's settings plus source_ready (not persisted): reactions_enabled, push_target (app | app_synapse | synapse), completion_notify (always | important | never), instructions, source_ready.
PUT /synapse-settingsFull save (reactions_enabled, push_target, completion_notify, instructions). When instructions change, records an edit version (edited_by="user"). Returns the saved settings + source_ready.
GET /synapse-settings/versions?limit=20Instruction edit history, newest first: content, edited_by (user | navi), reason, created_at. limit 1..100.
POST /synapse-targets · GET /synapse-targets · DELETE /synapse-targets/{id}Per-user Synapse s2s target secrets: create (token_ref + secret, secret is write-only), list (id, token_ref, created_at), revoke.
Per-user credentials for MCP servers whose config declares a user_key slot (see mcp.md). Auth required. Keys are never returned in any response — only presence and update time.
GET /mcp-keysEvery MCP server referenced by at least one profile, keyed ones first, with a flag marking which of them accept a personal key. Response 200:
{"items": [
{
"server_name": "gnexus-creds",
"transport": "streamable_http",
"accepts_user_key": true,
"key_type": "header",
"key_location": "Authorization",
"prefix": "Bearer ",
"has_key": false,
"updated_at": null,
"profiles": ["discuss", "server_admin"]
},
{
"server_name": "navi-web",
"transport": "stdio",
"accepts_user_key": false,
"key_type": null,
"key_location": null,
"prefix": null,
"has_key": false,
"updated_at": null,
"profiles": ["developer", "secretary"]
}
]}
key_type / key_location / prefix are null for servers without a slot; the per-user store is not queried at all when no listed server has one.
PUT /mcp-keys/{server_name}Body {"key": "..."} (1..4096 chars, trimmed). Saves the caller's key (Fernet-encrypted at rest) and invalidates the cached resolution. 404 for an unknown or key-less server, 422 for an empty/oversized key.
DELETE /mcp-keys/{server_name}Drops the caller's key. An admin then falls back to the config default; anyone else simply loses the server — it disappears from their tool list and a call to it is refused. 204 always when the server exists (even if no key was stored), 404 for unknown/key-less.
Client static: GET /static/** — served from client/ directory. Header Cache-Control: no-store.
Session uploaded files: stored in session_files/{session_id}/. Agent accesses them via the filesystem tool. Deleted after 24 h of session inactivity or when the session is deleted.
| HTTP | Reason |
|---|---|
400 |
Forbidden file type |
404 |
Session or profile not found |
413 |
File exceeds 200 MB |
500 |
Internal agent error |
WS 4004 |
Session not found on connect |