API Reference
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: Nerve does not implement its own auth layer — it relies on network-level access control (localhost binding, CORS allowlist). The gateway token is only exposed to loopback clients via
/api/connect-defaults. See SECURITY.md for details.
Table of Contents
- Health
- Server Info
- Version
- Connect Defaults
- Events (SSE)
- Text-to-Speech
- Transcription
- Token Usage
- Memories
- Agent Log
- Gateway
- Git Info
- Workspace Files
- Cron Jobs
- Skills
- File Serving
- Codex Limits
- Claude Code Limits
- Error Handling
- Rate Limiting
Health
GET /health
Health check with gateway connectivity probe.
Rate Limit: None
Response:
{
"status": "ok",
"uptime": 3621.42,
"gateway": "ok"
}| Field | Type | Description |
|---|---|---|
status | string | Always "ok" if the server is running |
uptime | number | Server 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"
}| Field | Type | Description |
|---|---|---|
serverTime | number | Current epoch milliseconds |
gatewayStartedAt | number | null | Gateway process start time (epoch ms). Linux only; null elsewhere |
timezone | string | IANA timezone of the server |
agentName | string | Configured agent display name |
Version
GET /api/version
Returns the application name and version from package.json.
Rate Limit: General (60/min)
Response:
{
"version": "1.3.0",
"name": "openclaw-nerve"
}Connect Defaults
GET /api/connect-defaults
Provides gateway WebSocket URL and auth token for the frontend's auto-connect feature. The gateway token is only returned to loopback clients — remote clients receive null.
Rate Limit: General (60/min)
Response (loopback):
{
"wsUrl": "ws://127.0.0.1:18789/ws",
"token": "your-gateway-token",
"agentName": "Agent"
}Response (remote):
{
"wsUrl": "ws://127.0.0.1:18789/ws",
"token": null,
"agentName": "Agent"
}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: noEvent 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
| Event | Trigger | Data Fields |
|---|---|---|
connected | On initial connection | { ts } |
ping | Every 30 seconds (keep-alive) | { ts } |
memory.changed | Memory file modified via API | { source, action, section?, file? } |
tokens.updated | Token usage changed | varies |
status.changed | Gateway status changed | varies |
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/mpeg binary.
Rate Limit: TTS (10/min)
Request Body:
{
"text": "Hello, world!",
"provider": "openai",
"voice": "alloy",
"model": "tts-1"
}| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Text to synthesize (1–5000 chars, non-empty after trim) |
provider | "openai" | "replicate" | "edge" | "qwen" | No | TTS provider. "qwen" is an alias for "replicate" + model "qwen-tts" |
voice | string | No | Provider-specific voice name |
model | string | No | Provider-specific model ID |
Provider Selection (when provider is omitted):
- OpenAI — if
OPENAI_API_KEYis set - Replicate — if
REPLICATE_API_TOKENis set - Edge TTS — always available (free, no API key)
Response: audio/mpeg binary (200)
Errors:
| Status | Condition |
|---|---|
| 400 | Validation failure (empty text, exceeds 5000 chars) |
| 429 | Rate limit exceeded |
| 500 | TTS 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": "preset", "speaker": "Chelsie" },
"openai": { "model": "tts-1", "voice": "alloy" },
"edge": { "voice": "en-US-AriaNeural" }
}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" }
}Allowed keys:
| Section | Fields |
|---|---|
qwen | mode, language, speaker, voiceDescription, styleInstruction |
openai | model, voice, instructions |
edge | voice |
All values must be strings, max 2000 characters each.
Transcription
POST /api/transcribe
Transcribes audio via OpenAI Whisper.
Rate Limit: Transcribe (30/min)
Body Size Limit: 12 MB
Requires: OPENAI_API_KEY
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:
| Status | Condition |
|---|---|
| 400 | No file in request |
| 413 | File exceeds 12 MB |
| 415 | Unsupported audio format |
| 500 | API key not configured / transcription failed |
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
GET /api/memories
Returns parsed memory data from MEMORY.md (sections + bullet items) and the 7 most recent daily files (section headers only).
Rate Limit: General (60/min)
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:
| Param | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Section title (exact match) |
date | string | No | YYYY-MM-DD for daily files; omit for MEMORY.md |
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
}| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Memory text (1–10000 chars) |
section | string | No | Section heading (default: "General", max 200 chars) |
category | "preference" | "fact" | "decision" | "entity" | "other" | No | Category for vector store |
importance | number | No | 0–1 importance score (default: 0.7) |
Response:
{ "ok": true, "result": { "written": true, "section": "Preferences" } }Broadcasts memory.changed SSE event on success.
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"
}| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Section title (1–200 chars) |
content | string | Yes | New markdown content (max 50000 chars) |
date | string | No | YYYY-MM-DD for daily files; omit for MEMORY.md |
DELETE /api/memories
Deletes a memory entry from the file.
Request Body:
{
"query": "Prefers dark mode",
"type": "item",
"date": "2025-02-15"
}| Field | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Text to find (exact match for items, section title for sections) |
type | "section" | "item" | "daily" | No | What to delete. section/daily removes header + all content. Default: item |
date | string | No | YYYY-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).
Rate Limit: None
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 available AI models from the OpenClaw gateway. Models are fetched via openclaw models list CLI and cached for 5 minutes.
Rate Limit: General (60/min)
Response:
{
"models": [
{ "id": "anthropic/claude-sonnet-4-20250514", "label": "claude-sonnet-4-20250514", "provider": "anthropic" },
{ "id": "openai/gpt-4o", "label": "gpt-4o", "provider": "openai" }
]
}Selection logic:
- Configured/allowlisted models (from
agents.defaults.modelsin OpenClaw config) — all included regardless ofavailableflag - If ≤0 results: falls back to all available models from the gateway
GET /api/gateway/session-info
Returns the current session's model and thinking level.
Query Parameters:
| Param | Default | Description |
|---|---|---|
sessionKey | agent:main:main | Session 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
Changes the model and/or thinking level for a session. HTTP fallback when WebSocket RPC fails.
Rate Limit: General (60/min)
Request Body:
{
"sessionKey": "agent:main:main",
"model": "anthropic/claude-sonnet-4-20250514",
"thinkingLevel": "high"
}Response:
{ "ok": true, "model": "anthropic/claude-sonnet-4-20250514", "thinking": "high" }Errors: 400 (invalid JSON), 502 (gateway tool invocation failed)
Git Info
GET /api/git-info
Returns the current git branch and dirty status.
Rate Limit: General (60/min)
Query Parameters:
| Param | Description |
|---|---|
sessionKey | Use a registered session-specific working directory |
Response:
{ "branch": "main", "dirty": true }Returns { "branch": null, "dirty": false } if not in a git repo.
POST /api/git-info/workdir
Registers a working directory for a session, so GET /api/git-info?sessionKey=... resolves to the correct repo.
Request Body:
{ "sessionKey": "agent:main:subagent:abc123", "workdir": "/home/user/project" }The workdir must be within the allowed base directory (derived from WORKSPACE_ROOT env var, git worktree list, or the parent of process.cwd()). Returns 403 if the path is outside the allowed base.
Session workdir entries expire after 1 hour. Max 100 entries.
DELETE /api/git-info/workdir
Unregisters a session's working directory.
Request Body:
{ "sessionKey": "agent:main:subagent:abc123" }Workspace Files
GET /api/workspace
Lists available workspace file keys and their existence status.
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
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." }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": { "name": "Check email", "schedule": "*/30 * * * *", "prompt": "Check email", "channel": "webchat", "model": "anthropic/claude-sonnet-4-20250514", "thinkingLevel": "medium", "enabled": true } }PATCH /api/crons/:id
Updates a cron job.
Request Body:
{ "patch": { "name": "Morning check", "schedule": "0 9 * * *", "prompt": "Check email", "model": "anthropic/claude-sonnet-4-20250514", "thinkingLevel": "medium", "channel": "webchat", "enabled": true } }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.
Rate Limit: General (60/min)
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 Serving
GET /api/files
Serves local image files with strict security controls. See SECURITY.md for the full threat model.
Query Parameters:
| Param | Description |
|---|---|
path | Absolute or ~-prefixed path to the image file |
GET /api/files?path=/tmp/screenshot.png
GET /api/files?path=~/.openclaw/workspace/memory/image.jpgAllowed 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:
| Status | Condition |
|---|---|
| 400 | Missing path parameter |
| 403 | Not an allowed file type, or path outside allowed directories (including after symlink resolution) |
| 404 | File not found |
| 500 | Read 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"
}
}Error Handling
All unhandled errors return:
HTTP 500
Content-Type: text/plain
Internal server errorIn 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.
| Preset | Routes | Limit |
|---|---|---|
| TTS | POST /api/tts | 10 requests / 60 seconds |
| Transcribe | POST /api/transcribe | 30 requests / 60 seconds |
| General | All other /api/* routes | 60 requests / 60 seconds |
Rate limit headers are included on every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58When exceeded:
HTTP 429
Retry-After: 45
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708100060Client 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:
- Error handler — catches unhandled exceptions
- Logger — request/response logging (Hono built-in)
- CORS — restricts to configured origins (see CONFIGURATION.md)
- Security headers — CSP, HSTS, X-Frame-Options, etc. (see SECURITY.md)
- Body limit — ~13 MB global max on
/api/*routes - Compression — gzip/deflate/brotli on all routes except
/api/events(SSE) - Cache headers —
no-cachefor API routes, immutable for hashed assets
Static Files & SPA
/assets/*— Vite-built static assets, served fromdist/- All non-API routes — SPA fallback to
dist/index.htmlfor client-side routing - Hashed assets (e.g.
index-Pbmes8jg.js) getCache-Control: public, max-age=31536000, immutable - Non-hashed files get
Cache-Control: public, max-age=0, must-revalidate