All API routes are Next.js App Router route handlers under app/api/.
The base URL during development is http://localhost:3000.
| Dependency | Required By | Notes |
|---|---|---|
OpenClaw gateway (localhost:18789) |
/api/chat/[id], /api/tts, /api/transcribe, /api/kanban/chat/[id] |
Must be running for any AI-powered route |
WORKSPACE_PATH env var |
/api/agents, /api/memory, /api/cron-runs, /api/kanban/chat-history/[ticketId] |
Filesystem path to .openclaw/workspace |
OPENCLAW_BIN env var |
/api/crons, /api/chat/[id] (vision path) |
Path to the openclaw CLI binary |
OPENCLAW_GATEWAY_TOKEN env var |
All gateway-dependent routes | Auth token for the OpenClaw gateway |
All error responses share a consistent JSON shape:
{ "error": "Human-readable error message" }Returned with the appropriate HTTP status code and Content-Type: application/json.
Returns the full list of registered agents, each with their SOUL.md content loaded from the filesystem.
Data source: JSON registry file (bundled lib/agents.json or user override at $WORKSPACE_PATH/clawport/agents.json) + SOUL.md files from the workspace filesystem.
No parameters.
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json |
Agent[] |
| 500 | application/json |
{ "error": string } |
Agent schema:
| Field | Type | Description |
|---|---|---|
id |
string |
Slug identifier (e.g. "vera") |
name |
string |
Display name (e.g. "VERA") |
title |
string |
Role title (e.g. "Chief Strategy Officer") |
reportsTo |
string | null |
Parent agent ID, or null for the root |
directReports |
string[] |
Child agent IDs |
soulPath |
string | null |
Path to the agent's SOUL.md file |
soul |
string | null |
Full SOUL.md content (loaded at request time), or null if file not found |
voiceId |
string | null |
ElevenLabs voice ID |
color |
string |
Hex color for the org chart node |
emoji |
string |
Emoji identifier |
tools |
string[] |
Tools this agent has access to |
crons |
CronJob[] |
Always [] from this endpoint (populated client-side) |
memoryPath |
string | null |
Path to the agent's memory file |
description |
string |
One-liner description of the agent |
curl http://localhost:3000/api/agentsconst res = await fetch('/api/agents')
const agents = await res.json()
// agents[0].id => "jarvis"
// agents[0].soul => "# JARVIS\n\nYou are the team's orchestrator..."Send a chat message to an agent and receive a streaming response. This route has two pipelines depending on whether the latest user message contains images.
Requires: OpenClaw gateway running at localhost:18789.
| Param | Type | Description |
|---|---|---|
id |
string |
Agent ID (must match a registered agent) |
| Field | Type | Required | Description |
|---|---|---|---|
messages |
ApiMessage[] |
Yes | Conversation history |
operatorName |
string |
No | Name shown to the agent as its operator. Defaults to "Operator" |
ApiMessage schema:
| Field | Type | Description |
|---|---|---|
role |
"user" | "assistant" | "system" |
Message role |
content |
string | ContentPart[] |
Plain text or multimodal content array |
ContentPart variants:
{ type: "text", text: string }
{ type: "image_url", image_url: { url: string } }Image URLs must be base64 data URLs (e.g. data:image/jpeg;base64,...). Client-side images should be resized to 1200px max before encoding to avoid exceeding macOS ARG_MAX.
Used when the latest user message does not contain images.
The route creates a streaming chat completion via the OpenAI SDK pointed at the gateway (localhost:18789/v1/chat/completions) using model claude-sonnet-4-6.
Response: Server-Sent Events (text/event-stream).
Each SSE data line is a JSON object with a content field containing the next token:
data: {"content":"Hello"}
data: {"content":" there"}
data: [DONE]
Used when the latest user message does contain image_url content parts and OPENCLAW_GATEWAY_TOKEN is set.
The gateway's /v1/chat/completions endpoint strips image content, so vision messages go through the CLI agent pipeline instead:
- Images are extracted and converted to
{ mimeType, content (base64) }attachments. openclaw gateway call chat.sendis invoked viaexecFileto send the message asynchronously.- The route polls
openclaw gateway call chat.historyevery 2 seconds (up to 60s timeout) until the assistant's response appears. - The complete response is returned as a single SSE frame followed by
[DONE].
Response: Same SSE format as Pipeline 1, but the entire response arrives in a single data: frame rather than streamed token-by-token.
| Status | Content-Type | Body |
|---|---|---|
| 200 | text/event-stream |
SSE stream (both pipelines) |
| 400 | application/json |
{ "error": string } -- invalid JSON or failed message validation |
| 404 | application/json |
{ "error": "Agent not found" } |
| 500 | application/json |
{ "error": "Chat failed. Make sure OpenClaw gateway is running." } |
// Text message
const res = await fetch('/api/chat/jarvis', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
operatorName: 'John',
messages: [
{ role: 'user', content: 'What cron jobs are running today?' }
]
})
})
const reader = res.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value)
// Parse SSE lines: "data: {\"content\":\"...\"}\n\n"
}// Vision message (image)
const res = await fetch('/api/chat/vera', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'What do you see in this screenshot?' },
{ type: 'image_url', image_url: { url: 'data:image/jpeg;base64,/9j/4AAQ...' } }
]
}
]
})
})Returns all cron jobs registered with OpenClaw, enriched with schedule descriptions, agent ownership, and delivery config.
Data source: Runs openclaw cron list --json via the CLI (OPENCLAW_BIN required).
No parameters.
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json |
CronJob[] |
| 500 | application/json |
{ "error": string } |
CronJob schema:
| Field | Type | Description |
|---|---|---|
id |
string |
Job identifier |
name |
string |
Job name (used to match owning agent by prefix) |
schedule |
string |
Raw cron expression |
scheduleDescription |
string |
Human-readable (e.g. "Daily at 8 AM") |
timezone |
string | null |
Timezone from schedule object, if present |
status |
"ok" | "error" | "idle" |
Last run outcome |
lastRun |
string | null |
ISO 8601 timestamp of last execution |
nextRun |
string | null |
ISO 8601 timestamp of next scheduled run |
lastError |
string | null |
Error message from last failed run |
agentId |
string | null |
Owning agent ID (matched by job name prefix) |
description |
string | null |
Job description |
enabled |
boolean |
Whether the job is active |
delivery |
CronDelivery | null |
Delivery config (mode, channel, to) |
lastDurationMs |
number | null |
Duration of last run in milliseconds |
consecutiveErrors |
number |
Count of consecutive failed runs |
lastDeliveryStatus |
string | null |
Delivery outcome of last run |
CronDelivery schema:
| Field | Type | Description |
|---|---|---|
mode |
string |
Delivery mode |
channel |
string |
Delivery channel |
to |
string | null |
Delivery recipient |
curl http://localhost:3000/api/cronsReturns cron run history parsed from JSONL log files on the filesystem. Results are sorted newest-first.
Data source: Reads .jsonl files from $WORKSPACE_PATH/../cron/runs/.
| Param | Type | Required | Description |
|---|---|---|---|
jobId |
string |
No | Filter to runs for a specific job. When provided, reads only {jobId}.jsonl. When omitted, reads all .jsonl files in the runs directory. |
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json |
CronRun[] |
| 500 | application/json |
{ "error": string } |
CronRun schema:
| Field | Type | Description |
|---|---|---|
ts |
number |
Unix timestamp (milliseconds) of the run |
jobId |
string |
Job identifier |
status |
"ok" | "error" |
Run outcome |
summary |
string | null |
Summary of what the run produced |
error |
string | null |
Error message if the run failed |
durationMs |
number |
Duration in milliseconds |
deliveryStatus |
string | null |
Delivery outcome |
# All runs
curl http://localhost:3000/api/cron-runs
# Runs for a specific job
curl "http://localhost:3000/api/cron-runs?jobId=pulse-daily-digest"Returns the contents of key memory files from the workspace: long-term memory, team memory, team intel, and the daily logs for today and yesterday.
Data source: Reads specific files from the $WORKSPACE_PATH filesystem directory.
Files checked (in order):
$WORKSPACE_PATH/MEMORY.md-- Long-Term Memory (Jarvis)$WORKSPACE_PATH/memory/team-memory.md-- Team Memory$WORKSPACE_PATH/memory/team-intel.json-- Team Intel (JSON)$WORKSPACE_PATH/memory/{YYYY-MM-DD}.md-- Daily Log (Today)$WORKSPACE_PATH/memory/{YYYY-MM-DD}.md-- Daily Log (Yesterday)
Only files that exist are included in the response.
No parameters.
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json |
MemoryFile[] |
| 500 | application/json |
{ "error": string } |
MemoryFile schema:
| Field | Type | Description |
|---|---|---|
label |
string |
Human-readable label (e.g. "Long-Term Memory (Jarvis)") |
path |
string |
Absolute filesystem path to the file |
content |
string |
Full file contents |
lastModified |
string |
ISO 8601 timestamp of last modification |
curl http://localhost:3000/api/memoryconst res = await fetch('/api/memory')
const files = await res.json()
// files[0].label => "Long-Term Memory (Jarvis)"
// files[0].content => "# Memory\n\n..."Converts text to speech audio using the OpenClaw gateway's TTS endpoint (OpenAI-compatible audio.speech API).
Requires: OpenClaw gateway running at localhost:18789.
| Field | Type | Required | Description |
|---|---|---|---|
text |
string |
Yes | The text to synthesize |
voice |
string |
No | Voice identifier. Defaults to "alloy" |
| Status | Content-Type | Body |
|---|---|---|
| 200 | audio/mpeg |
Raw MP3 audio bytes |
| 400 | application/json |
{ "error": "Missing or invalid \"text\" field" } |
| 500 | application/json |
{ "error": "TTS failed. Make sure OpenClaw gateway is running." } |
The Content-Length header is set on successful responses.
curl -X POST http://localhost:3000/api/tts \
-H 'Content-Type: application/json' \
-d '{"text": "Hello from Jarvis", "voice": "alloy"}' \
--output speech.mp3const res = await fetch('/api/tts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'Hello from Jarvis', voice: 'nova' })
})
const audioBlob = await res.blob()
const audioUrl = URL.createObjectURL(audioBlob)Transcribes audio to text using the OpenClaw gateway's Whisper endpoint (OpenAI-compatible audio.transcriptions API).
Requires: OpenClaw gateway running at localhost:18789.
Multipart form data (multipart/form-data).
| Field | Type | Required | Description |
|---|---|---|---|
audio |
File |
Yes | Audio file (webm, mp4, wav, etc.) |
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json |
{ "text": string } |
| 400 | application/json |
{ "error": "Expected multipart form data" } or { "error": "Missing audio file" } |
| 500 | application/json |
{ "error": "Transcription failed. Check OpenClaw gateway." } |
curl -X POST http://localhost:3000/api/transcribe \
-F 'audio=@recording.webm'const formData = new FormData()
formData.append('audio', audioBlob, 'recording.webm')
const res = await fetch('/api/transcribe', { method: 'POST', body: formData })
const { text } = await res.json()
// text => "Hello, what are the latest metrics?"Send a chat message to an agent in the context of a kanban ticket. Similar to the main chat route but includes ticket context in the system prompt. Text-only (no vision pipeline).
Requires: OpenClaw gateway running at localhost:18789.
| Param | Type | Description |
|---|---|---|
id |
string |
Agent ID (must match a registered agent) |
| Field | Type | Required | Description |
|---|---|---|---|
messages |
KanbanMessage[] |
Yes | Conversation history |
ticket |
Ticket |
No | Ticket context to include in the system prompt |
KanbanMessage schema:
| Field | Type | Description |
|---|---|---|
role |
"user" | "assistant" |
Message role |
content |
string |
Message text |
Ticket schema:
| Field | Type | Description |
|---|---|---|
title |
string |
Ticket title |
description |
string |
Ticket description |
status |
string |
Current status |
priority |
string |
Priority level |
assigneeRole |
string | null |
Role of the assigned agent |
workResult |
string | null |
Previous work output (included in prompt so the agent can reference it) |
| Status | Content-Type | Body |
|---|---|---|
| 200 | text/event-stream |
SSE stream (same format as /api/chat/[id]) |
| 400 | application/json |
{ "error": string } -- invalid JSON or messages not an array |
| 404 | application/json |
{ "error": "Agent not found" } |
| 500 | application/json |
{ "error": "Chat failed. Make sure OpenClaw gateway is running." } |
SSE format is identical to the main chat route's text pipeline:
data: {"content":"I see this ticket is about..."}
data: {"content":" the daily digest."}
data: [DONE]
const res = await fetch('/api/kanban/chat/pulse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [
{ role: 'user', content: 'What is the status of this ticket?' }
],
ticket: {
title: 'Fix daily digest formatting',
description: 'The email digest has broken HTML in the header.',
status: 'in-progress',
priority: 'high',
assigneeRole: 'pulse',
workResult: null
}
})
})Retrieve the persisted chat history for a kanban ticket.
Data source: Reads from $WORKSPACE_PATH/../kanban/chats/{ticketId}.jsonl on the filesystem.
| Param | Type | Description |
|---|---|---|
ticketId |
string |
Ticket identifier |
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json |
StoredChatMessage[] (sorted oldest-first) |
| 500 | application/json |
{ "error": string } |
Returns an empty array [] if no chat history file exists for the ticket.
StoredChatMessage schema:
| Field | Type | Description |
|---|---|---|
id |
string |
Unique message identifier |
role |
"user" | "assistant" |
Message role |
content |
string |
Message text |
timestamp |
number |
Unix timestamp (milliseconds) |
curl http://localhost:3000/api/kanban/chat-history/ticket-abc-123Append chat messages to the persisted history for a kanban ticket. Creates the chats directory and JSONL file if they do not exist.
Data source: Appends to $WORKSPACE_PATH/../kanban/chats/{ticketId}.jsonl on the filesystem.
| Param | Type | Description |
|---|---|---|
ticketId |
string |
Ticket identifier |
| Field | Type | Required | Description |
|---|---|---|---|
messages |
StoredChatMessage[] |
Yes | Messages to append (must be a non-empty array) |
See StoredChatMessage schema in the GET endpoint above.
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json |
{ "ok": true } |
| 400 | application/json |
{ "error": "messages array required" } |
| 500 | application/json |
{ "error": string } |
await fetch('/api/kanban/chat-history/ticket-abc-123', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [
{ id: 'msg-1', role: 'user', content: 'Can you look into this?', timestamp: 1709400000000 },
{ id: 'msg-2', role: 'assistant', content: 'On it.', timestamp: 1709400005000 }
]
})
})| Method | Endpoint | Gateway Required | Data Source | Content-Type |
|---|---|---|---|---|
| GET | /api/agents |
No | Filesystem (JSON + SOUL.md) | application/json |
| POST | /api/chat/[id] |
Yes | Gateway (streaming) or CLI (vision) | text/event-stream |
| GET | /api/crons |
No | CLI (openclaw cron list) |
application/json |
| GET | /api/cron-runs |
No | Filesystem (JSONL) | application/json |
| GET | /api/memory |
No | Filesystem (Markdown/JSON) | application/json |
| POST | /api/tts |
Yes | Gateway (audio.speech) |
audio/mpeg |
| POST | /api/transcribe |
Yes | Gateway (audio.transcriptions) |
application/json |
| POST | /api/kanban/chat/[id] |
Yes | Gateway (streaming) | text/event-stream |
| GET | /api/kanban/chat-history/[ticketId] |
No | Filesystem (JSONL) | application/json |
| POST | /api/kanban/chat-history/[ticketId] |
No | Filesystem (JSONL) | application/json |
All streaming chat endpoints (/api/chat/[id] and /api/kanban/chat/[id]) use the same Server-Sent Events protocol:
- Each data frame is a JSON object:
data: {"content":"token text"}\n\n - The stream terminates with:
data: [DONE]\n\n - Content-Type is
text/event-streamwithCache-Control: no-cacheandConnection: keep-alive. - If a stream error occurs mid-response, the server sends
[DONE]and closes the connection (no error frame is sent).
async function readStream(response) {
const reader = response.body.getReader()
const decoder = new TextDecoder()
let fullText = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const payload = line.slice(6)
if (payload === '[DONE]') return fullText
try {
const { content } = JSON.parse(payload)
fullText += content
} catch { /* skip malformed frames */ }
}
}
}
return fullText
}