API Reference

April 1, 2026 · View on GitHub

Nerve exposes a REST + SSE API served by Hono on the configured PORT (default 3080). All API routes are prefixed with /api/ except the health endpoint. Responses are JSON unless otherwise noted.

Authentication: When NERVE_AUTH=true, all API endpoints (except /api/auth/* and /health) require a valid session cookie. Obtain one via POST /api/auth/login. When NERVE_AUTH=false (default for localhost), no authentication is required. See SECURITY.md for details.


Table of Contents


Authentication

GET /api/auth/status

Check whether authentication is enabled and whether the current request is authenticated.

Rate Limit: None (public endpoint)

Response:

{
  "authEnabled": true,
  "authenticated": false
}
FieldTypeDescription
authEnabledbooleanWhether NERVE_AUTH is enabled on the server
authenticatedbooleanWhether the current request has a valid session cookie. Always true when auth is disabled.

POST /api/auth/login

Authenticate with a password and receive a session cookie.

Rate Limit: General (60/min)

Request Body:

{
  "password": "your-password"
}

Success Response (200):

{ "ok": true }

Sets an HttpOnly session cookie (nerve_session_{PORT}) on success.

Error Responses:

StatusBodyDescription
400{ "error": "Password required" }Empty or missing password
401{ "error": "Invalid password" }Wrong password

Notes:

  • When auth is disabled, always returns { "ok": true } without checking password.
  • Accepts the gateway token as a fallback password when no password hash is configured.

POST /api/auth/logout

Clear the session cookie.

Rate Limit: None (public endpoint)

Response:

{ "ok": true }

Health

GET /health

Health check with gateway connectivity probe.

Rate Limit: None

Response:

{
  "status": "ok",
  "uptime": 3621.42,
  "gateway": "ok"
}
FieldTypeDescription
statusstringAlways "ok" if the server is running
uptimenumberServer uptime in seconds
gateway"ok" | "unreachable"Result of a 3-second gateway health probe

Server Info

GET /api/server-info

Returns server time, gateway process uptime, timezone, and agent name.

Rate Limit: General (60/min)

Response:

{
  "serverTime": 1708100000000,
  "gatewayStartedAt": 1708090000000,
  "timezone": "Europe/Berlin",
  "agentName": "Agent"
}
FieldTypeDescription
serverTimenumberCurrent epoch milliseconds
gatewayStartedAtnumber | nullGateway process start time (epoch ms), when the host OS can resolve it (Linux via /proc, macOS via ps)
timezonestringIANA timezone of the server
agentNamestringConfigured agent display name

Version

GET /api/version

Returns the application name and version from package.json.

Rate Limit: None

Response:

{
  "version": "1.3.0",
  "name": "openclaw-nerve"
}

Connect Defaults

GET /api/connect-defaults

Provides the official gateway WebSocket URL and trust metadata for the frontend's auto-connect flow. The token field is always null; when serverSideAuth is true, Nerve expects the browser to connect with an empty token and injects GATEWAY_TOKEN server-side during the WebSocket handshake.

Rate Limit: General (60 requests / minute)

Response (trusted / auto-connect path):

{
  "wsUrl": "ws://127.0.0.1:18789/ws",
  "token": null,
  "agentName": "Agent",
  "authEnabled": false,
  "serverSideAuth": true
}

Response (manual token still required):

{
  "wsUrl": "ws://127.0.0.1:18789/ws",
  "token": null,
  "agentName": "Agent",
  "authEnabled": true,
  "serverSideAuth": false
}

serverSideAuth becomes true when Nerve can safely inject the configured gateway token for this request, such as:

  • loopback / tunneled local access to the official gateway URL
  • authenticated sessions on a network-exposed Nerve instance

If the browser is pointed at a custom gateway URL, or the request is not trusted for server-side injection, the connect dialog keeps the token field visible and the user must supply it manually.


Events (SSE)

GET /api/events

Server-Sent Events stream for real-time push updates. Compression is disabled on this route to prevent chunk buffering.

Headers sent by server:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

Event format:

Each SSE message has an event field and a JSON data payload:

event: memory.changed
data: {"event":"memory.changed","data":{"source":"api","action":"create","section":"General"},"ts":1708100000000}

Event Types

EventTriggerData Fields
connectedOn initial connection{ ts }
pingEvery 30 seconds (keep-alive){ ts }
memory.changedMemory file modified via API or file watcher{ source, action?, section?, file?, agentId? }
file.changedWorkspace file modified by the file watcher{ path, agentId }
tokens.updatedToken usage changedvaries
status.changedGateway status changedvaries

POST /api/events/test

Development only (NODE_ENV=development). Broadcasts a test event.

Body:

{
  "event": "test",
  "data": { "message": "Hello" }
}

Text-to-Speech

POST /api/tts

Synthesizes speech from text. Returns raw audio binary.

Rate Limit: TTS (10/min)
Body Size Limit: 64 KB

Request Body:

{
  "text": "Hello, world!",
  "provider": "openai",
  "voice": "alloy",
  "model": "tts-1"
}
FieldTypeRequiredDescription
textstringYesText to synthesize (1–5000 chars, non-empty after trim)
provider"openai" | "replicate" | "edge" | "qwen" | "xiaomi"NoTTS provider. "qwen" is a legacy alias for "replicate" + model "qwen-tts"
voicestringNoProvider-specific voice name
modelstringNoProvider-specific model ID

Provider Selection (when provider is omitted):

  1. OpenAI, if OPENAI_API_KEY is set
  2. Replicate, if REPLICATE_API_TOKEN is set
  3. Edge TTS, always available (free, no API key)

Xiaomi MiMo is available when provider: "xiaomi" is requested and MIMO_API_KEY is configured. It is not part of the automatic fallback chain.

Response: audio/mpeg binary for OpenAI, Replicate, and Edge, or audio/wav for Xiaomi MiMo (200)

Errors:

StatusCondition
400Validation failure (empty text, exceeds 5000 chars)
429Rate limit exceeded
500TTS synthesis failed

Results are cached in-memory keyed by provider:model:voice:text (MD5 hash). Cache: up to TTS_CACHE_MAX entries (default 200), TTL TTS_CACHE_TTL_MS (default 1 hour).

GET /api/tts/config

Returns the current TTS voice configuration.

Response:

{
  "qwen": {
    "mode": "voice_design",
    "language": "English",
    "speaker": "Serena",
    "voiceDescription": "",
    "styleInstruction": ""
  },
  "openai": {
    "model": "gpt-4o-mini-tts",
    "voice": "nova",
    "instructions": "Speak naturally and conversationally, like a real person. Warm, friendly tone with a slight British accent. Keep it casual and relaxed, not robotic or overly formal."
  },
  "edge": { "voice": "en-US-AriaNeural" },
  "xiaomi": { "model": "mimo-v2-tts", "voice": "mimo_default", "style": "" }
}

PUT /api/tts/config

Partially updates the TTS voice configuration. Only known keys are accepted.

Request Body (partial update):

{
  "openai": { "voice": "nova", "instructions": "Speak cheerfully" },
  "edge": { "voice": "en-GB-SoniaNeural" },
  "xiaomi": { "voice": "default_en", "style": "Happy" }
}

Allowed keys:

SectionFields
qwenmode, language, speaker, voiceDescription, styleInstruction
openaimodel, voice, instructions
edgevoice
xiaomimodel, voice, style

All values must be strings, max 2000 characters each.

GET /api/keys

Returns whether optional provider keys are configured. Key values are never returned.

Response:

{
  "openaiKeySet": true,
  "replicateKeySet": false,
  "xiaomiKeySet": true
}

PUT /api/keys

Writes optional provider keys to .env and hot-reloads the in-memory config.

Request Body (partial update):

{
  "openaiKey": "sk-...",
  "replicateToken": "r8_...",
  "mimoApiKey": "sk-mimo-..."
}

Any subset of fields may be provided. Sending an empty string clears that key.

Response:

{
  "ok": true,
  "message": "OPENAI_API_KEY saved, MIMO_API_KEY saved",
  "openaiKeySet": true,
  "replicateKeySet": false,
  "xiaomiKeySet": true
}

Transcription

POST /api/transcribe

Transcribes audio using the configured STT provider.

Rate Limit: Transcribe (30/min)
Body Size Limit: 12 MB

Providers:

ProviderConfigRequirements
openaiSTT_PROVIDER=openaiOPENAI_API_KEY
localSTT_PROVIDER=localffmpeg (auto-installed). Downloads whisper model on first use

Local STT uses @fugood/whisper.node (whisper.cpp bindings). Available models:

ModelSizeSpeedQuality
tiny75 MBFastestGood baseline, multilingual
base (default)142 MBFastBetter conversational accuracy, multilingual
small466 MBModerateBest accuracy (CPU-intensive), multilingual
tiny.en75 MBFastestEnglish-only variant
base.en142 MBFastEnglish-only variant
small.en466 MBModerateEnglish-only variant

Configure model via WHISPER_MODEL. Language hints come from NERVE_LANGUAGE (or PUT /api/language / PUT /api/transcribe/config). Models auto-download from HuggingFace on first use and are stored in WHISPER_MODEL_DIR (default ~/.nerve/models/).

Request: multipart/form-data with a file field containing audio data.

Accepted MIME types: audio/webm, audio/mp3, audio/mpeg, audio/mp4, audio/m4a, audio/wav, audio/x-wav, audio/ogg, audio/flac, audio/x-flac

curl -X POST http://localhost:3080/api/transcribe \
  -F "file=@recording.webm"

Response:

{
  "text": "The transcribed text goes here."
}

Errors:

StatusCondition
400No file in request
413File exceeds 12 MB
415Unsupported audio format
500API key not configured / transcription failed

GET /api/transcribe/config

Returns current STT runtime config + local model readiness/download state.

Response (example):

{
  "provider": "local",
  "model": "base",
  "language": "en",
  "modelReady": true,
  "openaiKeySet": false,
  "replicateKeySet": true,
  "hasGpu": false,
  "availableModels": {
    "tiny": { "size": "75MB", "ready": true, "multilingual": true },
    "base": { "size": "142MB", "ready": true, "multilingual": true },
    "tiny.en": { "size": "75MB", "ready": true, "multilingual": false }
  },
  "download": null
}

PUT /api/transcribe/config

Hot-reloads STT config at runtime.

Request Body (partial):

{
  "provider": "local",
  "model": "base",
  "language": "tr"
}
FieldTypeDescription
provider"local" | "openai"STT provider
modelstringWhisper model id (tiny, base, small, plus .en variants)
languagestringISO 639-1 language code (en, tr, de, etc.)

Language changes persist to .env as NERVE_LANGUAGE.


Language & Voice Phrases

GET /api/language

Returns current language settings and compatibility flags.

Rate Limit: General (60/min)

Response (example):

{
  "language": "en",
  "edgeVoiceGender": "female",
  "supported": [
    { "code": "en", "name": "English", "nativeName": "English" },
    { "code": "tr", "name": "Turkish", "nativeName": "Türkçe" }
  ],
  "providers": {
    "edge": true,
    "qwen3": true,
    "openai": true
  }
}

PUT /api/language

Hot-reloads language settings at runtime.

Rate Limit: General (60/min)

Request Body (partial):

{
  "language": "tr",
  "edgeVoiceGender": "male"
}
FieldTypeDescription
languagestringISO 639-1 language code
edgeVoiceGender"female" | "male"Preferred Edge TTS voice gender

Persists to .env keys:

  • NERVE_LANGUAGE
  • EDGE_VOICE_GENDER

GET /api/language/support

Returns full provider × language support matrix and current local model state.

Rate Limit: General (60/min)

Response (shape):

{
  "languages": [
    {
      "code": "en",
      "name": "English",
      "nativeName": "English",
      "edgeTtsVoices": { "female": "en-US-AriaNeural", "male": "en-US-GuyNeural" },
      "stt": { "local": true, "openai": true },
      "tts": { "edge": true, "qwen3": true, "openai": true }
    }
  ],
  "currentModel": "base",
  "isMultilingual": true
}

GET /api/voice-phrases

Returns merged phrase set for recognition matching (selected language + English fallback).

Query Parameters:

ParamTypeRequiredDescription
langstringNoISO 639-1 code. Defaults to current server language

Response:

{
  "stopPhrases": ["gönder", "send it"],
  "cancelPhrases": ["iptal", "cancel"],
  "wakePhrases": ["selam kim"]
}

GET /api/voice-phrases/status

Returns whether each supported language has custom phrase overrides configured.

Response (example):

{
  "en": { "configured": false, "hasDefaults": true },
  "tr": { "configured": true, "hasDefaults": true }
}

GET /api/voice-phrases/:lang

Returns language-only phrase config (no English merge).

Response:

{
  "source": "custom",
  "stopPhrases": ["gönder"],
  "cancelPhrases": ["iptal"],
  "wakePhrases": ["selam kim"]
}

source is one of custom, defaults, or none.

PUT /api/voice-phrases/:lang

Saves per-language custom phrase overrides.

Request Body (partial):

{
  "stopPhrases": ["gönder"],
  "cancelPhrases": ["iptal"],
  "wakePhrases": ["selam kim"]
}

At least one of stopPhrases, cancelPhrases, or wakePhrases is required.

Custom phrase overrides are stored in ~/.nerve/voice-phrases.json (created on first save).


Token Usage

GET /api/tokens

Returns token usage statistics from session transcript files, plus persistent cumulative totals.

Rate Limit: General (60/min)

Response:

{
  "totalCost": 1.2345,
  "totalInput": 500000,
  "totalOutput": 120000,
  "totalMessages": 85,
  "entries": [
    {
      "source": "anthropic",
      "cost": 0.9812,
      "messageCount": 60,
      "inputTokens": 400000,
      "outputTokens": 100000,
      "cacheReadTokens": 250000,
      "errorCount": 2
    }
  ],
  "persistent": {
    "totalInput": 1500000,
    "totalOutput": 400000,
    "totalCost": 5.6789,
    "lastUpdated": "2025-02-15T10:00:00Z"
  },
  "updatedAt": 1708100000000
}

Session data is cached for 60 seconds to avoid repeated filesystem scans.


Memories

All memory routes accept an optional agentId scope. If omitted, Nerve uses main. The UI normally derives this from the owning top-level agent when you switch sessions.

GET /api/memories

Returns parsed memory data from MEMORY.md (sections + bullet items) and the 7 most recent daily files (section headers only) for the selected workspace agent.

Rate Limit: General (60/min)

Query Parameters:

ParamTypeRequiredDescription
agentIdstringNoWorkspace agent id. Defaults to main

Response:

[
  { "type": "section", "text": "Preferences" },
  { "type": "item", "text": "Prefers dark mode" },
  { "type": "daily", "date": "2025-02-15", "text": "Worked on API docs" }
]

GET /api/memories/section

Returns the raw markdown content of a specific section.

Query Parameters:

ParamTypeRequiredDescription
titlestringYesSection title (exact match)
datestringNoYYYY-MM-DD for daily files; omit for MEMORY.md
agentIdstringNoWorkspace agent id. Defaults to main

Response:

{
  "ok": true,
  "content": "- Prefers dark mode\n- Likes TypeScript"
}

Errors: 400 (missing title, invalid date), 404 (file/section not found)

POST /api/memories

Creates a new memory entry. Writes a bullet point to MEMORY.md under the specified section (creating it if needed), and optionally stores in the gateway's vector database.

Rate Limit: General (60/min)

Request Body:

{
  "text": "Prefers dark mode",
  "section": "Preferences",
  "category": "preference",
  "importance": 0.8
}
FieldTypeRequiredDescription
textstringYesMemory text (1–10000 chars)
sectionstringNoSection heading (default: "General", max 200 chars)
category"preference" | "fact" | "decision" | "entity" | "other"NoCategory for vector store
importancenumberNo0–1 importance score (default: 0.7)
agentIdstringNoWorkspace agent id. Defaults to main

Response:

{ "ok": true, "result": { "written": true, "section": "Preferences" } }

Broadcasts memory.changed SSE event on success, tagged with the affected agentId.

PUT /api/memories/section

Replaces the content of an existing section.

Request Body:

{
  "title": "Preferences",
  "content": "- Prefers dark mode\n- Likes TypeScript",
  "date": "2025-02-15"
}
FieldTypeRequiredDescription
titlestringYesSection title (1–200 chars)
contentstringYesNew markdown content (max 50000 chars)
datestringNoYYYY-MM-DD for daily files; omit for MEMORY.md
agentIdstringNoWorkspace agent id. Defaults to main

DELETE /api/memories

Deletes a memory entry from the file.

Request Body:

{
  "query": "Prefers dark mode",
  "type": "item",
  "date": "2025-02-15"
}
FieldTypeRequiredDescription
querystringYesText to find (exact match for items, section title for sections)
type"section" | "item" | "daily"NoWhat to delete. section/daily removes header + all content. Default: item
agentIdstringNoWorkspace agent id. Defaults to main
datestringNoYYYY-MM-DD — required when type is "daily"

Response:

{ "ok": true, "result": { "deleted": 1, "source": "file", "file": "MEMORY.md", "type": "item" } }

Agent Log

GET /api/agentlog

Returns the full agent log (JSON array, max 200 entries).

Response:

[
  { "ts": 1708100000000, "type": "action", "message": "Started task", "level": "info" }
]

POST /api/agentlog

Appends an entry to the agent log.

Rate Limit: General (60/min)

Request Body:

{
  "type": "action",
  "message": "Completed deployment",
  "level": "info",
  "data": { "duration": 45 }
}

All fields are optional. A ts (epoch ms) is automatically set on write. The log is capped at 200 entries (oldest trimmed on write).


Gateway

GET /api/gateway/models

Returns the models defined in the active OpenClaw config. This endpoint is config-backed now, not CLI-discovered or cache-backed.

Rate Limit: General (60/min)

Response:

{
  "models": [
    {
      "id": "anthropic/claude-sonnet-4-20250514",
      "label": "claude-sonnet-4-20250514",
      "provider": "anthropic",
      "configured": true,
      "role": "primary"
    }
  ],
  "error": null,
  "source": "config"
}
FieldTypeDescription
modelsarrayConfigured models from agents.defaults.model and agents.defaults.models in the active OpenClaw config
errorstring | nullRead error or configuration problem, for example config unreadable or no configured models
source"config"Identifies the backing source

Model roles are primary, fallback, or allowed. If the config cannot be read, or no models are configured, the endpoint returns an empty models array with an explanatory error string.

GET /api/gateway/session-info

Returns the current session's model and thinking level.

Query Parameters:

ParamDefaultDescription
sessionKeyagent:main:mainSession identifier

Response:

{
  "model": "anthropic/claude-opus-4-6",
  "thinking": "medium"
}

Resolution order: per-session data from sessions_list → global gateway_status / status / session_status tools.

POST /api/gateway/session-patch

HTTP fallback for model changes when the frontend cannot apply sessions.patch over WebSocket. Thinking-only changes are not supported here.

Rate Limit: General (60/min)

Request Body:

{
  "sessionKey": "agent:main:main",
  "model": "anthropic/claude-sonnet-4-20250514",
  "thinkingLevel": "high"
}
FieldTypeRequiredDescription
sessionKeystringNoTarget session. If omitted, Nerve tries to pick a preferred active root session
modelstringNoModel to apply via the gateway's session_status tool
thinkingLevelstring | nullNoAccepted by the schema, but not applied by this HTTP fallback

Response:

{ "ok": true, "model": "anthropic/claude-sonnet-4-20250514" }

Behavior notes:

  • Thinking changes belong on WebSocket RPC sessions.patch, alongside other session metadata/settings updates.
  • A request that only changes thinkingLevel returns 501.
  • If no active root session can be found and sessionKey is omitted, the endpoint returns 409.
  • If both model and thinkingLevel are sent, the model change is applied and the thinking change is ignored.

Errors:

StatusCondition
400Invalid JSON or validation error
409No active root session available
501Thinking-only changes are not supported over HTTP
502Model change failed

POST /api/gateway/restart

Restarts the OpenClaw gateway service, then waits for the service to report healthy status and for the gateway port to become reachable.

Rate Limit: Restart (3/min)

Response:

{ "ok": true, "output": "Gateway restarted successfully" }

Errors: 500 if restart or post-restart verification fails.


Sessions

GET /api/sessions/hidden

Returns hidden cron-like sessions from sessions.json, sorted by recent activity. Used to surface session metadata that is not part of the normal active session tree.

Rate Limit: General (60/min)

Query Parameters:

ParamDefaultDescription
activeMinutes1440Include sessions updated within the last N minutes
limit200Maximum results. Clamped to 2000

Response:

{
  "ok": true,
  "sessions": [
    {
      "key": "agent:main:cron:daily:run:abc",
      "sessionKey": "agent:main:cron:daily:run:abc",
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "label": "daily summary",
      "displayName": "daily summary",
      "updatedAt": 1708100000000,
      "model": "openai/gpt-5",
      "thinking": "medium",
      "thinkingLevel": "medium",
      "totalTokens": 1234,
      "contextTokens": 456,
      "parentId": "agent:main:cron:daily"
    }
  ]
}

If the backing sessions.json file is unavailable, the endpoint returns { "ok": true, "sessions": [] }. In remote-workspace cases it may also include remoteWorkspace: true.

GET /api/sessions/:id/model

Reads the actual model used by a session from its transcript. This is mainly for cron-run sessions where gateway session listings may only expose the parent agent's default model.

Rate Limit: General (60/min)

Path Parameters:

ParamDescription
idSession UUID

Response:

{ "ok": true, "model": "openai/gpt-5", "missing": false }

If the transcript cannot be found, the endpoint returns { "ok": true, "model": null, "missing": true }.

Errors: 400 if id is not a valid UUID.


Workspace Files

Workspace file routes accept an optional agentId scope. If omitted, Nerve uses the main workspace.

GET /api/workspace

Lists available workspace file keys and their existence status for the selected workspace agent.

Query Parameters:

ParamTypeRequiredDescription
agentIdstringNoWorkspace agent id. Defaults to main

Response:

{
  "ok": true,
  "files": [
    { "key": "soul", "filename": "SOUL.md", "exists": true },
    { "key": "tools", "filename": "TOOLS.md", "exists": true },
    { "key": "identity", "filename": "IDENTITY.md", "exists": false },
    { "key": "user", "filename": "USER.md", "exists": true },
    { "key": "agents", "filename": "AGENTS.md", "exists": true },
    { "key": "heartbeat", "filename": "HEARTBEAT.md", "exists": false }
  ]
}

GET /api/workspace/:key

Reads a workspace file by key.

Valid keys: soul, tools, identity, user, agents, heartbeat

Query Parameters:

ParamTypeRequiredDescription
agentIdstringNoWorkspace agent id. Defaults to main

Response:

{ "ok": true, "content": "# SOUL.md\n\nYou are..." }

Errors: 400 (unknown key), 404 (file not found)

PUT /api/workspace/:key

Writes content to a workspace file.

Request Body:

{ "content": "# Updated content\n\nNew text here.", "agentId": "research" }
FieldTypeRequiredDescription
contentstringYesNew file contents
agentIdstringNoWorkspace agent id. Defaults to main

Content must be a string, max 100 KB.


Cron Jobs

All cron routes proxy to the OpenClaw gateway via invokeGatewayTool('cron', ...).

Rate Limit: General (60/min) on all endpoints.

GET /api/crons

Lists all cron jobs (including disabled).

Response:

{ "ok": true, "result": { "jobs": [...] } }

POST /api/crons

Creates a new cron job.

Request Body:

{ "job": { "schedule": "*/30 * * * *", "prompt": "Check email", "channel": "webchat" } }

PATCH /api/crons/:id

Updates a cron job.

Request Body:

{ "patch": { "schedule": "0 9 * * *" } }

DELETE /api/crons/:id

Deletes a cron job.

POST /api/crons/:id/toggle

Toggles a cron job's enabled state.

Request Body:

{ "enabled": false }

POST /api/crons/:id/run

Triggers immediate execution of a cron job. Timeout: 60 seconds.

GET /api/crons/:id/runs

Returns the last 10 run history entries for a cron job.

All cron errors return 502 when the gateway tool invocation fails.


Skills

GET /api/skills

Lists all OpenClaw skills via openclaw skills list --json for the selected workspace agent.

Nerve scopes this by creating a temporary OpenClaw config whose agents.defaults.workspace points at the selected agent workspace, then runs the CLI against that workspace.

Rate Limit: General (60/min)

Query Parameters:

ParamTypeRequiredDescription
agentIdstringNoWorkspace agent id. Defaults to main

Response:

{
  "ok": true,
  "skills": [
    {
      "name": "web-search",
      "description": "Search the web using Brave Search API",
      "emoji": "🔍",
      "eligible": true,
      "disabled": false,
      "blockedByAllowlist": false,
      "source": "/home/user/.openclaw/skills/web-search",
      "bundled": true,
      "homepage": "https://github.com/example/skill"
    }
  ]
}

File Browser

Browse, read, and edit workspace files. All paths are restricted to the selected workspace directory with traversal protection.

All file-browser routes accept an optional agentId scope. If omitted, Nerve uses main. If FILE_BROWSER_ROOT is set, file-browser agent scoping is bypassed and all sessions browse the same custom root.

GET /api/files/tree

Returns the workspace directory tree. Excludes node_modules, .git, dist, server-dist, and other build artifacts.

Query Parameters:

ParamTypeRequiredDescription
pathstringNoRelative directory path to expand. Defaults to root
depthnumberNoExpansion depth, clamped to 1–5
agentIdstringNoWorkspace agent id. Defaults to main

Response:

{
  "ok": true,
  "root": ".",
  "entries": [
    {
      "name": "MEMORY.md",
      "path": "MEMORY.md",
      "type": "file",
      "mtime": 1771355007542
    },
    {
      "name": "memory",
      "path": "memory",
      "type": "directory",
      "children": null
    }
  ],
  "workspaceInfo": {
    "isCustomWorkspace": false,
    "rootPath": "/home/user/.openclaw/workspace"
  }
}

GET /api/files/read

Read a text file's contents with its modification time (for conflict detection on save).

Query Parameters:

ParamTypeRequiredDescription
pathstringYesRelative path within the selected workspace
agentIdstringNoWorkspace agent id. Defaults to main

Response:

{
  "ok": true,
  "content": "# MEMORY.md\n...",
  "size": 128,
  "mtime": 1771355007542
}

Errors:

StatusCondition
400Missing path, not a file, or other invalid request
403Path traversal or excluded path
404File not found
413File too large
415Binary file

PUT /api/files/write

Write text file contents with optimistic concurrency via mtime comparison. If the file was modified since it was last read, returns 409 Conflict.

Request Body:

{
  "path": "MEMORY.md",
  "content": "# Updated content\n...",
  "expectedMtime": 1771355007542,
  "agentId": "research"
}
FieldTypeRequiredDescription
pathstringYesRelative file path within the selected workspace
contentstringYesUTF-8 file contents
expectedMtimenumberNoExpected current mtime for optimistic concurrency
agentIdstringNoWorkspace agent id. Defaults to main

Response (success):

{
  "ok": true,
  "mtime": 1771355107000
}

Errors:

StatusCondition
400Missing fields or invalid request
403Path traversal or excluded path
409File modified since last read (currentMtime returned)
413Content too large
415Binary file write attempted

POST /api/files/rename

Rename a file or directory within the selected workspace.

Request Body:

{ "path": "notes/today.md", "newName": "tomorrow.md", "agentId": "research" }

POST /api/files/move

Move a file or directory into another directory within the selected workspace.

Request Body:

{ "sourcePath": "notes/today.md", "targetDirPath": "archive", "agentId": "research" }

POST /api/files/trash

Move a file or directory into .trash when using the default workspace root. If FILE_BROWSER_ROOT is set, deletion is permanent instead.

Request Body:

{ "path": "notes/today.md", "agentId": "research" }

POST /api/files/restore

Restore an item from .trash back to its original location.

Request Body:

{ "path": ".trash/2026-03-20-notes-today.md", "agentId": "research" }

GET /api/files/raw

Serves supported image files from the selected workspace for previews.

Query Parameters:

ParamTypeRequiredDescription
pathstringYesRelative image path
agentIdstringNoWorkspace agent id. Defaults to main

Supported image types: png, jpg, jpeg, gif, webp, avif, svg, ico.


File Serving

GET /api/files

Serves local image files with strict security controls. See SECURITY.md for the full threat model.

Query Parameters:

ParamDescription
pathAbsolute or ~-prefixed path to the image file
GET /api/files?path=/tmp/screenshot.png
GET /api/files?path=~/.openclaw/workspace/memory/image.jpg

Allowed file types: .png, .jpg, .jpeg, .gif, .webp, .svg, .avif

Allowed directory prefixes: /tmp, ~/.openclaw, the configured MEMORY_DIR

Response: Raw image binary with appropriate Content-Type header and 1-hour cache.

Errors:

StatusCondition
400Missing path parameter
403Not an allowed file type, or path outside allowed directories (including after symlink resolution)
404File not found
500Read failure

Codex Limits

GET /api/codex-limits

Returns OpenAI Codex rate limit information. Tries the API first (requires ~/.codex/auth.json), then falls back to parsing local session files.

Response:

{
  "available": true,
  "source": "api",
  "five_hour_limit": {
    "used_percent": 45.2,
    "left_percent": 54.8,
    "resets_at": 1708110000,
    "resets_at_formatted": "14:00"
  },
  "weekly_limit": {
    "used_percent": 12.0,
    "left_percent": 88.0,
    "resets_at": 1708300000,
    "resets_at_formatted": "17 Feb, 14:00"
  },
  "credits": {
    "has_credits": true,
    "unlimited": false,
    "balance": 50.0
  },
  "plan_type": "pro"
}

Claude Code Limits

GET /api/claude-code-limits

Returns Claude Code rate limit information by spawning the CLI parser. Reset times are normalised to epoch milliseconds.

Response:

{
  "available": true,
  "session_limit": {
    "used_percent": 30.0,
    "left_percent": 70.0,
    "resets_at_epoch": 1708110000000,
    "resets_at_raw": "7:59pm (UTC)"
  },
  "weekly_limit": {
    "used_percent": 8.5,
    "left_percent": 91.5,
    "resets_at_epoch": 1708300000000,
    "resets_at_raw": "Feb 18, 6:59pm"
  }
}

Kanban

Task board with drag-and-drop columns, agent execution, and a proposal workflow. Tasks flow through a state machine: backlogtodoin-progressreviewdone. See Architecture for design details.

Task Object

All task endpoints return a KanbanTask object:

FieldTypeDescription
idstringUUID
titlestringTask title (1--500 chars)
descriptionstring | undefinedOptional description (max 10000 chars)
statusstringOne of: backlog, todo, in-progress, review, done, cancelled
prioritystringOne of: critical, high, normal, low
createdBystring"operator" or "agent:<label>"
createdAtnumberEpoch milliseconds
updatedAtnumberEpoch milliseconds
versionnumberCAS version (incremented on every mutation)
sourceSessionKeystring | undefinedOriginating session
assigneestring | undefined"operator" or "agent:<label>"
labelsstring[]Tags (max 50, each max 100 chars)
columnOrdernumberPosition within the status column
runobject | undefinedActive or last run link (see below)
resultstring | undefinedAgent output text (max 50000 chars)
resultAtnumber | undefinedWhen the result was set
modelstring | undefinedModel override for execution
thinkingstring | undefinedThinking level: off, low, medium, high
dueAtnumber | undefinedDue date (epoch ms)
estimateMinnumber | undefinedEstimated minutes
actualMinnumber | undefinedActual minutes
feedbackarrayReview feedback entries: { at, by, note }

Run link object (run):

FieldTypeDescription
sessionKeystringGateway session key
sessionIdstring | undefinedSession ID
runIdstring | undefinedRun ID
startedAtnumberEpoch milliseconds
endedAtnumber | undefinedEpoch milliseconds
statusstringrunning, done, error, aborted
errorstring | undefinedError message if failed

GET /api/kanban/tasks

List tasks with optional filters and pagination.

Rate Limit: General (60/min)

Query Parameters:

ParamTypeDefaultDescription
statusstring(all)Filter by status. Repeatable or comma-separated (e.g. ?status=todo,in-progress)
prioritystring(all)Filter by priority. Repeatable or comma-separated
assigneestring(all)Filter by assignee (exact match)
labelstring(all)Filter by label (exact match)
qstring(none)Search title, description, and labels (case-insensitive substring)
limitnumber50Page size (1--200)
offsetnumber0Pagination offset

Response:

{
  "items": [
    {
      "id": "a1b2c3d4-...",
      "title": "Refactor auth module",
      "status": "todo",
      "priority": "high",
      "version": 3,
      "labels": ["backend"],
      "columnOrder": 0,
      "createdBy": "operator",
      "createdAt": 1708100000000,
      "updatedAt": 1708100500000,
      "feedback": []
    }
  ],
  "total": 12,
  "limit": 50,
  "offset": 0,
  "hasMore": false
}

Tasks are sorted by status order → column order → most recently updated.

GET /api/kanban/tasks/:id

Fetch a single task by ID.

Rate Limit: General (60/min)

Response: The matching KanbanTask object.

Errors:

StatusBodyCondition
404{ "error": "not_found" }Task not found

POST /api/kanban/tasks

Create a new task.

Rate Limit: General (60/min)

Request Body:

{
  "title": "Refactor auth module",
  "description": "Extract session logic into its own service",
  "status": "todo",
  "priority": "high",
  "assignee": "operator",
  "labels": ["backend", "refactor"],
  "model": "anthropic/claude-sonnet-4-20250514",
  "thinking": "medium",
  "dueAt": 1708200000000,
  "estimateMin": 60
}
FieldTypeRequiredDescription
titlestringYesTask title (1--500 chars)
descriptionstringNoDescription (max 10000 chars)
statusstringNoInitial status. Defaults to board config defaults.status (usually todo)
prioritystringNoPriority. Defaults to board config defaults.priority (usually normal)
createdBystringNoCreator. Default: "operator"
sourceSessionKeystringNoOriginating session key (max 500 chars)
assigneestringNo"operator" or "agent:<label>"
labelsstring[]NoTags (max 50 items, each max 100 chars). Default: []
modelstringNoModel for agent execution (max 200 chars)
thinkingstringNoThinking level: off, low, medium, high
dueAtnumberNoDue date (epoch ms)
estimateMinnumberNoEstimated minutes (≥0)

Response (201): The created KanbanTask object.

Errors:

StatusCondition
400Validation error (missing title, invalid field values)

PATCH /api/kanban/tasks/:id

Update a task. Requires the current version for optimistic concurrency (CAS). Send null for nullable fields to clear them.

Rate Limit: General (60/min)

Request Body:

{
  "version": 3,
  "title": "Updated title",
  "priority": "critical",
  "assignee": null
}
FieldTypeRequiredDescription
versionnumberYesCurrent task version (for conflict detection)
titlestringNoUpdated title
descriptionstring | nullNoUpdated description. null to clear
statusstringNoNew status
prioritystringNoNew priority
assigneestring | nullNoNew assignee. null to clear
labelsstring[]NoReplace labels
modelstring | nullNoModel override. null to clear
thinkingstring | nullNoThinking level. null to clear
dueAtnumber | nullNoDue date. null to clear
estimateMinnumber | nullNoEstimated minutes. null to clear
actualMinnumber | nullNoActual minutes. null to clear
resultstring | nullNoResult text (max 50000 chars). null to clear

Response: The updated KanbanTask object.

Errors:

StatusBodyCondition
400{ "error": "validation_error", "details": "..." }Invalid field values
404{ "error": "not_found" }Task not found
409{ "error": "version_conflict", "serverVersion": 4, "latest": {...} }Version mismatch. Response includes the current task so you can retry

DELETE /api/kanban/tasks/:id

Delete a task permanently.

Rate Limit: General (60/min)

Response:

{ "ok": true }

Errors: 404 if task not found.

POST /api/kanban/tasks/:id/reorder

Move a task to a different position within its column or to another column. CAS-versioned.

Rate Limit: General (60/min)

Request Body:

{
  "version": 3,
  "targetStatus": "in-progress",
  "targetIndex": 0
}
FieldTypeRequiredDescription
versionnumberYesCurrent task version
targetStatusstringYesTarget column status
targetIndexnumberYesZero-based position in the target column

Response: The updated KanbanTask object.

Errors:

StatusCondition
400Invalid body
404Task not found
409Version conflict (returns serverVersion and latest task)

POST /api/kanban/tasks/:id/execute

Execute a task and move it to in-progress. The launch path depends on the task's assignee and platform. The task must be in todo or backlog status. Moves the task to in-progress and starts polling the agent session for completion.

Rate Limit: General (60/min)

Request Body (optional):

{
  "model": "anthropic/claude-sonnet-4-20250514",
  "thinking": "high"
}
FieldTypeRequiredDescription
modelstringNoExecution model override (max 200 chars). Cascade: execute request → task model → board defaultModel → OpenClaw configured default
thinkingstringNoThinking override: off, low, medium, high

Response: The updated KanbanTask object with status: "in-progress" and a run object.

Execution paths:

  • Assigned tasks create a real child session beneath the assignee's live root. Nerve verifies that the parent root exists, creates the child with sessions.create(parentSessionKey=...), then sends the task into that child with sessions.send.
  • Unassigned or operator tasks use the normal sessions_spawn path.
  • macOS fallback rule: unassigned or operator tasks are rejected. Assign the task to a live worker root first.

Errors:

StatusBodyCondition
404{ "error": "not_found" }Task not found
409{ "error": "duplicate_execution" }Task is already running
409{ "error": "invalid_execution_target" }Required parent root is missing, or macOS requires an assigned live worker root
409{ "error": "invalid_transition", "from": "done", "to": "in-progress" }Task not in todo or backlog status

Notes:

  • The spawned worker receives the task title and description as its prompt.
  • Assigned-task runs keep both a deterministic run correlation key and the real childSessionKey.
  • When an assigned child session finishes or fails, Nerve sends a completion report back to the parent root session.
  • Backend pollers run every 5 seconds for up to 720 attempts / 60 minutes.
  • On success the task moves to review. On error it moves back to todo.

POST /api/kanban/tasks/:id/complete

Complete a running task. Called by the backend poller automatically, but can also be called directly.

Rate Limit: General (60/min)

Request Body:

{
  "sessionKey": "kb-auth-refactor-123-v4-1708100000000",
  "result": "Refactored auth module. Extracted SessionService class...",
  "error": "Agent session timed out"
}
FieldTypeRequiredDescription
sessionKeystringYesActive run session key used to match the task run
resultstringNoAgent output text (max 50000 chars). Kanban markers are parsed and stripped automatically
errorstringNoError message (max 5000 chars). If set, task moves to todo instead of review

Response: The updated KanbanTask object.

Errors:

StatusCondition
400Invalid body or missing sessionKey
404Task not found
409No active matching run to complete

POST /api/kanban/tasks/:id/approve

Approve a task in review. Moves it to done.

Rate Limit: General (60/min)

Request Body (optional):

{
  "note": "Looks good, nice work"
}
FieldTypeRequiredDescription
notestringNoApproval note (max 5000 chars). Added to task feedback

Response: The updated KanbanTask object with status: "done".

Errors:

StatusCondition
404Task not found
409Task not in review status

POST /api/kanban/tasks/:id/reject

Reject a task in review. Moves it back to todo and clears the run and result so it can be re-executed.

Rate Limit: General (60/min)

Request Body:

{
  "note": "Missing error handling for edge cases"
}
FieldTypeRequiredDescription
notestringYesRejection reason (1--5000 chars). Added to task feedback

Response: The updated KanbanTask object with status: "todo".

Errors:

StatusCondition
400Missing or empty note
404Task not found
409Task not in review status

POST /api/kanban/tasks/:id/abort

Abort a running task. Marks the run as aborted and moves the task back to todo.

Rate Limit: General (60/min)

Request Body (optional):

{
  "note": "Taking too long, will retry with a different approach"
}
FieldTypeRequiredDescription
notestringNoAbort reason (max 5000 chars). Added to task feedback

Response: The updated KanbanTask object with status: "todo".

Errors:

StatusCondition
404Task not found
409Task not in-progress with an active run

GET /api/kanban/proposals

List kanban proposals (agent-suggested task changes).

Rate Limit: General (60/min)

Query Parameters:

ParamTypeDefaultDescription
statusstring(all)Filter by status: pending, approved, rejected

Response:

{
  "proposals": [
    {
      "id": "f8e7d6c5-...",
      "type": "create",
      "payload": { "title": "Add rate limiting to API", "priority": "high" },
      "proposedBy": "agent:kanban-abc123",
      "proposedAt": 1708100000000,
      "status": "pending",
      "version": 1
    }
  ]
}

Proposals are sorted most-recent first.

POST /api/kanban/proposals

Create a proposal manually. Typically proposals are created automatically when agents emit kanban markers, but this endpoint allows direct creation.

Rate Limit: General (60/min)

Request Body:

{
  "type": "create",
  "payload": {
    "title": "Add rate limiting to API",
    "priority": "high",
    "labels": ["backend"]
  }
}
FieldTypeRequiredDescription
typestringYes"create" or "update"
payloadobjectYesTask fields. Schema depends on type (see below)
sourceSessionKeystringNoOriginating session key (max 500 chars)
proposedBystringNoActor. Default: "operator"

Create payload fields: title (required), description, status, priority, assignee, labels, model, thinking, dueAt, estimateMin

Update payload fields: id (required -- references existing task), title, description, status, priority, assignee, labels, result

Response (201): The created proposal object.

Errors:

StatusCondition
400Invalid payload for the given type
404Update proposal references a non-existent task

Notes:

  • When proposalPolicy is "auto", the proposal is immediately applied (created as approved).
  • When proposalPolicy is "confirm" (default), the proposal stays pending until manually approved or rejected.

POST /api/kanban/proposals/:id/approve

Approve a pending proposal. Creates or updates the task based on the proposal type.

Rate Limit: General (60/min)

Response:

{
  "proposal": { "id": "...", "status": "approved", "resolvedAt": 1708100500000 },
  "task": { "id": "...", "title": "Add rate limiting to API" }
}

Errors:

StatusCondition
404Proposal not found
409Proposal already resolved ({ "error": "already_resolved", "proposal": {...} })

POST /api/kanban/proposals/:id/reject

Reject a pending proposal.

Rate Limit: General (60/min)

Request Body (optional):

{
  "reason": "Not a priority right now"
}
FieldTypeRequiredDescription
reasonstringNoRejection reason (max 5000 chars)

Response:

{
  "proposal": { "id": "...", "status": "rejected", "reason": "Not a priority right now" }
}

Errors:

StatusCondition
404Proposal not found
409Proposal already resolved

GET /api/kanban/config

Get the current board configuration.

Rate Limit: General (60/min)

Response:

{
  "columns": [
    { "key": "backlog", "title": "Backlog", "visible": true },
    { "key": "todo", "title": "To Do", "visible": true },
    { "key": "in-progress", "title": "In Progress", "visible": true },
    { "key": "review", "title": "Review", "visible": true },
    { "key": "done", "title": "Done", "visible": true },
    { "key": "cancelled", "title": "Cancelled", "visible": false }
  ],
  "defaults": {
    "status": "todo",
    "priority": "normal"
  },
  "reviewRequired": true,
  "allowDoneDragBypass": false,
  "quickViewLimit": 5,
  "proposalPolicy": "confirm"
}

PUT /api/kanban/config

Update board configuration. Partial updates -- only include the fields you want to change.

Rate Limit: General (60/min)

Request Body (partial):

{
  "proposalPolicy": "auto",
  "defaultModel": "anthropic/claude-sonnet-4-20250514",
  "quickViewLimit": 10
}

See Configuration -- Kanban for all available fields and defaults.

Response: The full updated config object.

Errors: 400 if validation fails.


Error Handling

All unhandled errors return:

HTTP 500
Content-Type: text/plain

Internal server error

In development (NODE_ENV=development), stack traces are logged to the server console but never sent to clients.

Validation errors from Zod schemas return 400 with a human-readable message from the first validation issue.


Rate Limiting

All /api/* routes have rate limiting applied. Limits are per-client-IP per-path using a sliding window.

PresetRoutesLimit
TTSPOST /api/tts10 requests / 60 seconds
TranscribePOST /api/transcribe30 requests / 60 seconds
GeneralMost /api/* routes60 requests / 60 seconds
RestartPOST /api/gateway/restart3 requests / 60 seconds

Rate limit headers are included on every response:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58

When exceeded:

HTTP 429
Retry-After: 45
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708100060

Client identification: Uses the real TCP socket address (not spoofable headers). X-Forwarded-For and X-Real-IP are only trusted when the direct connection comes from a trusted proxy (loopback by default; extend via TRUSTED_PROXIES env var).


Global Middleware

Applied to all requests in order:

  1. Error handler — catches unhandled exceptions
  2. Logger — request/response logging (Hono built-in)
  3. CORS — restricts to configured origins (see CONFIGURATION.md)
  4. Security headers — CSP, HSTS, X-Frame-Options, etc. (see SECURITY.md)
  5. Body limit — ~13 MB global max on /api/* routes
  6. Compression — gzip/deflate on all routes except /api/events (SSE)
  7. Cache headersno-cache for API routes, immutable for hashed assets

Static Files & SPA

  • /assets/* — Vite-built static assets, served from dist/
  • All non-API routes — SPA fallback to dist/index.html for client-side routing
  • Hashed assets (e.g. index-Pbmes8jg.js) get Cache-Control: public, max-age=31536000, immutable
  • Non-hashed files get Cache-Control: public, max-age=0, must-revalidate