Default port: Desktop App 19280, ESP32 WiFi 80
| Limit | Value | Description |
|---|---|---|
| Payload size | 10KB | Maximum request body size |
| Rate limit | 100 req/min | Per IP address |
| Request timeout | 30 sec | Prevents Slowloris attacks |
| CORS | localhost only | Only allows localhost origins |
Note: ESP32 HTTP server does not enforce these limits. ESP32 security relies on local network isolation and SSID sanitization.
| Field | Max Length | Format |
|---|---|---|
state |
- | One of valid states |
project |
100 chars | String |
tool |
50 chars | String |
model |
50 chars | String |
memory |
- | Integer 0-100 (context-window usage) |
usage5h |
- | Integer 0-100 (5-hour plan-usage window) |
usageWeek |
- | Integer 0-100 (weekly plan-usage window) |
character |
- | clawd, codex, kiro, claw, or daangni |
terminalId |
100 chars | Desktop only. Terminal session ID with prefix: iterm2:w0t0p0:UUID (from ITERM_SESSION_ID) or ghostty:12345 (from GHOSTTY_PID) |
characteris a visual rendering choice, typically selected by the agent bridge. It is not a general agent identity field.
| Endpoint | Desktop | ESP32 WiFi |
|---|---|---|
| GET / | ✓ | - |
| GET /dashboard-data | ✓ | - |
| POST/GET /status | ✓ | ✓ |
| GET /windows | ✓ | - |
| POST /close | ✓ | - |
| POST /show | ✓ | - |
| GET /health | ✓ | ✓ |
| GET /debug | ✓ | - |
| POST /quit | ✓ | - |
| POST /lock | ✓ | ✓ |
| POST /unlock | ✓ | ✓ |
| GET/POST /lock-mode | ✓ | ✓ |
| GET/POST /window-mode | ✓ | - |
| GET/POST /app-mode | ✓ | - |
| GET/POST /character-lock | ✓ | - |
| GET /stats | ✓ | - |
| GET /stats/data | ✓ | - |
| POST /reboot | - | ✓ |
| POST /wifi-reset | - | ✓ |
Update monitor status.
curl -X POST http://127.0.0.1:19280/status \
-H "Content-Type: application/json" \
-d '{"state":"working","tool":"Bash","project":"my-project"}'
Request Body:
| Field | Type | Description |
|---|---|---|
state |
string | start, idle, thinking, planning, working, packing, notification, done, sleep, alert |
tool |
string | Tool name (e.g., Bash, Read, Edit) |
project |
string | Project name |
model |
string | Model name (e.g., opus, sonnet) |
memory |
number | Context-window usage (0-100) |
usage5h |
number | 5-hour plan-usage window (0-100) |
usageWeek |
number | Weekly plan-usage window (0-100) |
character |
string | clawd, codex, kiro, claw, or daangni (Desktop only — ESP32 silently ignores daangni) |
terminalId |
string | Desktop only. Terminal ID for click-to-focus (e.g., iterm2:w0t0p0:UUID or ghostty:12345) |
Desktop: An unrecognized
statevalue is rejected with a400error. ESP32: An unrecognizedstatevalue is silently treated asidle— no error is returned.
Agent bridges usually set character automatically:
clawd for Claude Codecodex for Codexkiro for Kiroclaw for OpenClawResponse (Desktop):
{"success": true, "project": "my-project", "state": "working", "windowCount": 2}
skipped: trueis added when neitherstatenor the info fields (tool,model,memory,usage5h,usageWeek,character) changed (optimization). If a project is blocked (project locked or max windows),successisfalsewith anerrorfield, pluslockedProject(blocked by lock) orwindowCount(max windows reached).
Response (ESP32 WiFi):
{"success": true}
If blocked by project lock:
{"success": false, "blocked": true}
Get current status.
curl http://127.0.0.1:19280/status
Response (Desktop):
{
"windowCount": 2,
"projects": {
"my-project": {"state": "working", "tool": "Bash", "model": "opus", "memory": 45, "usage5h": 36, "usageWeek": 37},
"other-project": {"state": "idle"}
}
}
Response (ESP32 WiFi):
{
"state": "working",
"project": "my-project",
"lockedProject": "my-project",
"lockMode": "on-thinking",
"projectCount": 1
}
List all active windows with their states and positions.
curl http://127.0.0.1:19280/windows
Response:
{
"windowCount": 2,
"windows": [
{"project": "my-project", "state": "working", "bounds": {"x": 1748, "y": 23, "width": 172, "height": 348}},
{"project": "other-project", "state": "idle", "bounds": {"x": 1566, "y": 23, "width": 172, "height": 348}}
]
}
Close a specific project window.
curl -X POST http://127.0.0.1:19280/close \
-H "Content-Type: application/json" \
-d '{"project":"my-project"}'
Response:
{"success": true, "project": "my-project", "windowCount": 1}
Show window and position to top-right corner.
# Show first window
curl -X POST http://127.0.0.1:19280/show
# Show specific project window
curl -X POST http://127.0.0.1:19280/show \
-H "Content-Type: application/json" \
-d '{"project":"my-project"}'
Request Body (optional):
| Field | Type | Description |
|---|---|---|
project |
string | Project name to show (defaults to first window) |
Response:
{"success": true, "project": "my-project"}
When
projectis omitted, the response’sprojectfield is the literal string"first", not the actual project ID of the shown window.
Get current window mode.
curl http://127.0.0.1:19280/window-mode
Response:
{"mode": "multi", "windowCount": 2, "lockedProject": null}
Set window mode (multi or single).
curl -X POST http://127.0.0.1:19280/window-mode \
-H "Content-Type: application/json" \
-d '{"mode":"single"}'
Response:
{"success": true, "mode": "single", "windowCount": 1, "lockedProject": null}
On an invalid
mode, the response is{"success": false, "error": "Invalid mode: <mode>", "validModes": ["multi", "single"]}.
Get current app mode.
curl http://127.0.0.1:19280/app-mode
Response:
{"mode": "window", "windowCount": 2}
Set app mode (character, window, or input).
curl -X POST http://127.0.0.1:19280/app-mode \
-H "Content-Type: application/json" \
-d '{"mode":"character"}'
Response:
{"success": true, "mode": "character", "windowCount": 1}
On an invalid
mode, the response is{"success": false, "error": "Invalid mode: <mode>", "validModes": ["character", "window", "input"]}.
Get current character lock.
curl http://127.0.0.1:19280/character-lock
Response:
{"character": "auto"}
Force every window to show one character regardless of what each project’s status reports (auto, or one of clawd, codex, kiro, claw, daangni). auto restores each project’s own character on its next status update.
curl -X POST http://127.0.0.1:19280/character-lock \
-H "Content-Type: application/json" \
-d '{"character":"daangni"}'
Response:
{"success": true, "character": "daangni"}
On an invalid
character, the response is{"success": false, "error": "Invalid character: <character>", "validCharacters": ["auto", "clawd", "codex", "kiro", "claw", "daangni"]}.
Lock to a specific project.
# Desktop
curl -X POST http://127.0.0.1:19280/lock \
-H "Content-Type: application/json" \
-d '{"project":"my-project"}'
# ESP32
curl -X POST http://192.168.0.185/lock \
-H "Content-Type: application/json" \
-d '{"project":"my-project"}'
Request Body:
| Field | Type | Description |
|---|---|---|
project |
string | Project name to lock. Defaults to current project if omitted (ESP32 only). |
Response:
{"success": true, "lockedProject": "my-project"}
Desktop: Only works in single-window mode. Returns
{"success": false, "error": "Lock only available in single-window mode"}in multi-window mode. If the locked project has no active window, the response includes"warning": "No active window for this project".ESP32: Always available. When locking a new project, the display transitions to
idlestate and clearstool,model,memory.
Unlock project.
# Desktop
curl -X POST http://127.0.0.1:19280/unlock
# ESP32
curl -X POST http://192.168.0.185/unlock
Response:
{"success": true, "lockedProject": null}
Desktop: Only works in single-window mode. Returns
{"success": false, "error": "Unlock only available in single-window mode"}in multi-window mode.
Get current lock mode.
# Desktop
curl http://127.0.0.1:19280/lock-mode
# ESP32
curl http://192.168.0.185/lock-mode
Response (Desktop):
{
"mode": "on-thinking",
"modes": {"first-project": "First Project", "on-thinking": "On Thinking"},
"lockedProject": null,
"windowMode": "single"
}
Response (ESP32 WiFi):
{
"mode": "on-thinking",
"modes": {"first-project": "First Project", "on-thinking": "On Thinking"},
"lockedProject": null
}
windowModeis Desktop-only (ESP32 has no window mode concept).
Set lock mode (first-project or on-thinking).
# Desktop
curl -X POST http://127.0.0.1:19280/lock-mode \
-H "Content-Type: application/json" \
-d '{"mode":"first-project"}'
# ESP32
curl -X POST http://192.168.0.185/lock-mode \
-H "Content-Type: application/json" \
-d '{"mode":"first-project"}'
Response:
{"success": true, "mode": "first-project", "lockedProject": null}
ESP32: Changing lock mode resets the current lock (
lockedProjectbecomes null) and persists the new mode to Flash storage.Desktop: On an invalid
mode, the response is{"success": false, "error": "Invalid mode: <mode>", "validModes": [...]}listing the accepted mode keys.ESP32: On an invalid
mode, the response is HTTP 400 with{"error": "Invalid mode. Valid modes: first-project, on-thinking"}(nosuccessfield).
Serve the dashboard HTML page showing all active windows and current modes.
open http://127.0.0.1:19280/
Get current dashboard data as JSON (used by the dashboard page).
curl http://127.0.0.1:19280/dashboard-data
Response:
{
"health": "ok",
"windowCount": 2,
"windowMode": "multi",
"lockMode": "on-thinking",
"lockedProject": null,
"windows": [
{"project": "my-project", "state": "working"},
{"project": "other-project", "state": "idle"}
]
}
Serve the stats dashboard HTML page.
# Open in browser
open http://127.0.0.1:19280/stats
Get stats data from ~/.claude/stats-cache.json.
curl http://127.0.0.1:19280/stats/data
Response:
{
"sessions": [...],
"totalTokens": 12345,
"lastUpdated": "2026-01-29T12:00:00Z"
}
Errors:
404{"error": "Stats file not found: ~/.claude/stats-cache.json"}if the cache doesn’t exist;500{"error": "Failed to parse stats file: <message>"}or{"error": "Failed to read stats file: <message>"}on other failures.
Health check endpoint.
curl http://127.0.0.1:19280/health
Response:
{"status": "ok"}
Get display and window debug information.
curl http://127.0.0.1:19280/debug
Response:
{
"primaryDisplay": {"bounds": {"x": 0, "y": 0, "width": 1920, "height": 1080}, "workArea": {...}},
"allDisplays": [...],
"windows": [{"projectId": "my-project", "bounds": {...}, "state": "working"}],
"windowCount": 1,
"maxWindows": 5,
"alwaysOnTopMode": "active-only",
"platform": "darwin"
}
Quit the application.
curl -X POST http://127.0.0.1:19280/quit
Reboot the ESP32 device.
curl -X POST http://192.168.0.185/reboot \
-H "Content-Type: application/json" \
-d '{"confirm":true}'
Response:
{"success": true, "rebooting": true}
Clear saved WiFi credentials and return to provisioning mode.
curl -X POST http://192.168.0.185/wifi-reset \
-H "Content-Type: application/json" \
-d '{"confirm":true}'
Response:
{"success": true, "message": "WiFi credentials cleared. Rebooting..."}
Behavior:
wifiSSID, wifiPassword from NVS (WebSocket token is preserved)VibeMon-Setup AP)See ESP32 Setup Guide for details.
| Code | Desktop | ESP32 |
|---|---|---|
200 |
Success | Success (also used for some errors — check success field) |
400 |
Bad request (validation error) | Bad request (missing body or invalid input) |
404 |
Not found | Likely, via the WebServer library’s default unmatched-route handler (normal mode registers no custom onNotFound) |
408 |
Request timeout | - |
413 |
Payload too large (>10KB) | - |
429 |
Too many requests (rate limited) | - |
500 |
Internal server error | - |
ESP32 note: The ESP32 HTTP server always returns HTTP 200 for valid requests (including project-lock rejections). Check the
successfield in the response body to determine the outcome.
{"error": "Error message description"}
Desktop note: Routes not matching any known endpoint return a plain-text
404 Not Foundbody, not JSON.