REST API
Cloud Only
The Cloud REST API is available at https://api.adhf.dev. Self-hosted users can access a local API — see Self-hosted API.
This page documents the hosted cloud automation surface.
It does not try to cover:
- local standalone endpoints such as
/api/v1/status - self-hosted runtime operator flows such as session-host recovery
- local terminal mux endpoints such as
/api/v1/mux/*
For those, use the OSS self-hosted docs instead.
This page is an intentional public summary, not a trimmed copy that drifted. A fuller internal reference covering admin-gated and internal endpoints is maintained privately; where the two disagree, the internal reference is authoritative.
Authentication
All API requests require an API key in the Authorization header:
curl -H "Authorization: Bearer adk_..." \
https://api.adhf.dev/api/v1/daemonsNote: API key issuance via the dashboard is not available in the current release. Existing keys continue to work. Contact support if you need access.
For normal cloud automation, API keys are the main auth surface.
Some sensitive cloud-account mutations now require a recent browser login instead of API-key auth. Those routes expect a fresh dashboard JWT session and return 403 RECENT_LOGIN_REQUIRED when called with an API key or a stale session.
Current recent-login protected routes:
POST /api/v1/webhooksPATCH /api/v1/webhooks/{webhookId}DELETE /api/v1/webhooks/{webhookId}POST /api/v1/webhooks/{webhookId}/testPOST /api/v1/daemons/{daemonId}/disconnectPOST /api/v1/daemons/{daemonId}/revokePOST /api/v1/api-keys(create)DELETE /api/v1/api-keys/{keyId}(revoke)
Scopes
| Scope | Description |
|---|---|
ide:read | List daemons, read IDE state |
ide:control | Launch/stop IDEs, send daemon commands |
agent:read | Read chat messages, agent status |
agent:control | Send messages, approve/reject actions |
terminal:exec | Execute terminal commands on machines |
webhook:manage | Inspect deliveries and, with a recent dashboard login, manage webhooks |
Public API Shape
For most cloud integrations, think in these layers:
GET /api/v1/daemonsto find connected machinesGET /api/v1/daemons/{daemonId}/statusto inspect machine and session state/api/v1/shortcuts/*for the common agent actions you actually want/api/v1/webhooks/*for event delivery to your own systems
Prefer Shortcuts unless you specifically need the raw daemon command router.
All daemon and shortcut routes are scoped to machines and sessions owned by the authenticated cloud account. Supplying some other account's daemon or synthetic session id returns 404 instead of crossing account boundaries.
Daemons
Manage connected machines (daemons) and their IDEs.
List Daemons
GET /api/v1/daemonsScope: ide:read
Returns all connected machines with their managed IDEs and CLIs.
Response:
{
"daemons": [
{
"id": "c79baa8f...",
"hostname": "M1-Server",
"nickname": "작업용",
"platform": "darwin",
"cdpConnected": true,
"ides": [
{ "id": "c79baa8f:ide:cursor_myproject", "type": "cursor", "cdpConnected": true }
],
"clis": [
{ "id": "c79baa8f:cli:claude-cli", "type": "claude-cli", "name": "Claude Code" }
]
}
]
}Get Daemon Status
GET /api/v1/daemons/{daemonId}/statusScope: ide:read
Returns detailed daemon status including IDE/CLI state, workspace info, and agent activity.
Send Daemon Command
POST /api/v1/daemons/{daemonId}/commandScope: ide:control
Send a direct command to the target machine.
For most integrations, prefer the Shortcuts API below. POST /daemons/:id/command is the lower-level escape hatch.
Body:
{
"type": "send_chat",
"payload": { "message": "Hello!" }
}Available Command Types
| Command | Target | Payload | Description |
|---|---|---|---|
send_chat | IDE/CLI | message | Send a chat message |
read_chat | IDE/CLI | — | Read current chat contents |
resolve_action | IDE/CLI | action | Approve/reject agent action |
screenshot | IDE | width? | Take a screenshot |
launch_ide | Daemon | ideType, enableCdp? | Launch an IDE |
launch_cli | Daemon | cliType, dir?, model? | Start CLI session |
stop_cli | Daemon | cliType | Stop CLI session |
Not every command in the internal router belongs in public automation material. If you are building a normal integration, stay on the documented command set or the shortcut endpoints.
Execute Terminal Command
POST /api/v1/daemons/{daemonId}/terminalScope: terminal:exec
Execute a terminal command on the daemon machine.
Body:
{
"command": "ls -la",
"name": "my-terminal",
"cwd": "/home/user"
}Disconnect Daemon
POST /api/v1/daemons/{daemonId}/disconnectScope: ide:control
Force disconnect the daemon. The daemon can reconnect automatically.
This is a recent-login protected cloud action. API keys and stale browser sessions receive 403 RECENT_LOGIN_REQUIRED.
Revoke Daemon Token
POST /api/v1/daemons/{daemonId}/revokeScope: ide:control
Revoke the daemon's connection token permanently. Requires running adhdev setup again on the machine.
This is a recent-login protected cloud action. API keys and stale browser sessions receive 403 RECENT_LOGIN_REQUIRED.
Shortcuts
Convenience endpoints for the most common agent actions.
Launch IDE / CLI
POST /api/v1/shortcuts/{daemonId}/launchScope: ide:control
Body:
{ "type": "cursor" }{ "type": "claude-cli", "dir": "/path/to/project" }Stop CLI
POST /api/v1/shortcuts/{daemonId}/stopScope: ide:control
Body:
{ "type": "claude-cli" }Send Chat Message
POST /api/v1/shortcuts/{ideId}/chatScope: agent:control
Send a message to an AI agent. The ideId is the machine-scoped target ID returned by the daemon/session status APIs, including synthetic session routes like {daemonId}:session:{sessionId}.
This is the default send path for external automation.
Body (legacy text form):
{ "message": "Fix the bug in auth.ts" }Body (canonical envelope form):
{
"input": {
"parts": [
{ "type": "text", "text": "Fix the bug in auth.ts" }
],
"textFallback": "Fix the bug in auth.ts"
}
}Optional — specify agent type for multi-agent IDEs:
{ "message": "Fix the bug", "agentType": "cursor" }You must provide either message or input.
Read Chat
GET /api/v1/shortcuts/{ideId}/chatScope: agent:read
Read the current chat contents from an agent. Optionally filter by ?agentType=cursor.
Chat Debug Bundle
GET /api/v1/shortcuts/{ideId}/chat/debug
POST /api/v1/shortcuts/{ideId}/chat/debugScope: agent:read
Build a bounded, sanitized debug bundle for the current chat/session. The daemon generates the authoritative provider/parser/session/terminal evidence; POST may include a dashboard frontendSnapshot to supplement it with currently rendered frontend state.
Optional body for POST:
{ "agentType": "codex-cli", "frontendSnapshot": { "activeConversation": { "sessionId": "session_..." } } }The response includes bundle and copy-ready text. Secrets and credentials are redacted by default.
Approve / Reject Action
POST /api/v1/shortcuts/{ideId}/approveScope: agent:control
Approve or reject a pending agent action.
Body:
{ "action": "approve" }or
{ "action": "reject", "agentType": "cursor" }Get Agent Status
GET /api/v1/shortcuts/{ideId}/statusScope: agent:read
Returns current status (idle, generating, waiting_approval, error), provider summary metadata/controls when present, and workspace info.
Webhooks
Manage webhook subscriptions for real-time event notifications. See Webhooks feature guide for setup instructions and signature verification.
List Webhooks
GET /api/v1/webhooksScope: webhook:manage
Create Webhook
POST /api/v1/webhooksScope: webhook:manage
Body:
{
"url": "https://example.com/hook",
"events": ["agent:status", "agent:approve_request"]
}events is an allow-list — see Events below for which names can be subscribed to individually versus only via "*". Use ["*"] to subscribe to all events. The response includes a secret (shown only once) for signature verification.
This is a recent-login protected cloud action. API keys and stale browser sessions receive 403 RECENT_LOGIN_REQUIRED.
Toggle Webhook
PATCH /api/v1/webhooks/{webhookId}Scope: webhook:manage
Body:
{ "active": false }This is a recent-login protected cloud action. API keys and stale browser sessions receive 403 RECENT_LOGIN_REQUIRED.
Delete Webhook
DELETE /api/v1/webhooks/{webhookId}Scope: webhook:manage
This is a recent-login protected cloud action. API keys and stale browser sessions receive 403 RECENT_LOGIN_REQUIRED.
List Deliveries
GET /api/v1/webhooks/{webhookId}/deliveries?limit=20Scope: webhook:manage
Returns recent delivery history with status codes, response bodies, and timing.
Test Webhook
POST /api/v1/webhooks/{webhookId}/testScope: webhook:manage
Sends a test webhook:test event to the webhook URL.
This is a recent-login protected cloud action. API keys and stale browser sessions receive 403 RECENT_LOGIN_REQUIRED.
TURN Credentials
GET /api/v1/turn/credentialsIssues short-lived TURN/STUN credentials for the dashboard's own P2P connection (a daemon receives its credentials separately, over the WebSocket auth_ok message — this REST endpoint is dashboard-only). Requires an authenticated request (access token); an expired token returns 401 and the caller should fall back to STUN only.
TURN is enabled on every selling plan, including Free — this is a deliberate product decision, not a Free-tier restriction. The response falls back to STUN-only (turn: false) only when a plan has been given turnenabled: false via an admin override, or when the TURN service itself is unconfigured or returns an error.
Response:
{
"iceServers": [ { "urls": "stun:stun.cloudflare.com:3478" } /* + TURN servers when enabled */ ],
"turn": true,
"plan": "pro"
}Successful TURN credentials are cached server-side for 1 hour, so repeated calls within that window return cached: true with the same servers.
Provider Registry
GET /api/v1/registry/providers
GET /api/v1/registry/providers/{type}
GET /api/v1/registry/providers/{type}/{version}
GET /api/v1/registry/providers/{type}/{version}/downloadPublic, unauthenticated catalog of published provider packages — daemons use this to sync provider bundles. ?channel=preview opts into the preview namespace; omitting it (or any other value) returns stable. This registry delivery mechanism is a distinct, non-frozen capability, separate from the frozen dashboard UI for browsing providers.
Write endpoints are gated to admins or an API key carrying the narrower registry:publish scope:
POST /api/v1/registry/providers
POST /api/v1/registry/providers/{type}/{version}/promote
DELETE /api/v1/registry/providers/{type}/{version}POST /registry/providers publishes a new package and always requires an explicit channel: "preview" in the body — publishing directly to stable is rejected; a stable release only ever happens through the promote endpoint, which references the existing preview version's artifact rather than accepting new bytes. DELETE yanks a version (defaults to the stable channel unless ?channel=preview is given).
Events
These events are delivered to webhooks and also drive live cloud notification flows. Webhook events subscriptions are an allow-list, not a free-form list of names you'll receive (normalizeWebhookEvents in packages/server/src/utils/webhook-events.ts):
Individually subscribable:
| Event | Description |
|---|---|
daemon:connect | Machine came online |
daemon:disconnect | Machine went offline |
agent:status | Agent status changed (throttled, max 1 per 10s per daemon) |
agent:approve_request | Reserved — allow-listed but not currently dispatched by the server |
Dispatched but only delivered to webhooks subscribed with "*":
| Event | Description |
|---|---|
agent:generating_started | Agent started generating a response |
agent:generating_completed | Agent finished generating (carries duration in whole seconds when the daemon saw the turn start) |
agent:waiting_approval | Agent is waiting for user approval |
agent:waiting_choice | Agent is waiting on a multiple-choice prompt |
agent:stopped | Agent session stopped |
monitor:no_progress | Agent appears stalled with no progress (legacy alias: monitor:long_generating) |
webhook:test | Explicit test delivery via POST /webhooks/{id}/test — delivered only to the one webhook being tested, bypasses subscription filtering |
Event Payload
Every delivery is a JSON body with three top-level fields: event, payload, and timestamp (when the server sent the delivery, epoch milliseconds). For the agent and monitor events (agent:* except agent:status, and monitor:no_progress) the payload looks like this:
{
"event": "agent:generating_completed",
"payload": {
"daemonId": "8e1f4c2a9b7d3e5f6a0c1b2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f",
"daemonType": "adhdev-daemon",
"event": "agent:generating_completed",
"timestamp": 1714000000000,
"targetSessionId": "9b2d7c4e-1f3a-4b5c-8d6e-0a1b2c3d4e5f",
"providerType": "claude-cli",
"providerSessionId": "3f2a9c1e-5b7d-4e8a-9c0f-1a2b3c4d5e6f",
"workspaceName": "/home/dev/projects/myproject",
"duration": 42,
"surfaceHidden": false,
"muted": false
},
"timestamp": 1714000000215
}| Field | When present |
|---|---|
daemonId | Always — the daemon connection that reported the event |
event, timestamp | Always — the event name and when the daemon observed it (epoch milliseconds) |
targetSessionId | When the event concerns one agent session — always for agent:generating_completed and agent:stopped |
providerType | When the daemon can resolve the session. The agent provider, for example claude-cli or cursor. Absent otherwise — it never carries the daemon kind |
daemonType | Always. The kind of daemon that sent the event, currently adhdev-daemon |
providerSessionId | When the provider exposes its own conversation id |
workspaceName | When the session has a workspace — the working directory of a CLI agent, or the IDE workspace |
duration | agent:generating_completed only, in whole seconds, when the daemon saw the turn start |
elapsedSec | monitor:no_progress — seconds without visible progress |
modalMessage, modalButtons | agent:waiting_approval / agent:waiting_choice — the approval prompt text and its button labels |
surfaceHidden, muted | When the daemon hosts the session — whether it is hidden or muted on the dashboard |
daemon:connect, daemon:disconnect, agent:status, and webhook:test carry their own, different payloads. Apart from the approval prompt text in modalMessage, the payload never includes chat messages, chat titles, or agent responses.
Typical Flow
The most common API flow is:
- List machines with
GET /api/v1/daemons - Pick a target agent from the daemon status payload
- Send a message with
POST /api/v1/shortcuts/{ideId}/chat - Poll
GET /api/v1/shortcuts/{ideId}/statusor use webhooks - Read the transcript with
GET /api/v1/shortcuts/{ideId}/chat
What This Page Excludes
If you are looking for any of these, this is the wrong API page:
- standalone
GET /api/v1/status - standalone
POST /api/v1/command - runtime snapshot/events endpoints for hosted local CLI sessions
- terminal mux workspace endpoints
Those belong to Self-hosted API.
Rate Limits
Per-Request Limits
| Limit | Value |
|---|---|
| API Key | 120 requests / minute |
| IP address | 60 requests / minute |
| Auth login/callback endpoints | 100 requests / 5 minutes |
| Command timeout | 60 seconds |
Monthly Call Limits
| Plan | Monthly API Calls |
|---|---|
| Free | 1,000 |
| Pro | 50,000 |
| Ultra | 500,000 |
| Enterprise | Unlimited |
Error Codes
| Status | Code | Description |
|---|---|---|
400 | — | Bad request (validation failed) |
401 | AUTH_REQUIRED | Missing Authorization header |
401 | AUTH_INVALID | Invalid or expired API key |
403 | AUTH_FORBIDDEN | API key lacks required scope |
403 | RECENT_LOGIN_REQUIRED | Sensitive cloud action requires a fresh dashboard login instead of API-key auth or a stale session |
404 | — | Daemon/resource not found, expired invite/share, or target not owned by the authenticated account |
429 | RATE_LIMITED | Per-minute rate limit exceeded |
429 | API_LIMIT_EXCEEDED | Monthly API call limit reached |
500 | — | Command send failed (daemon offline) |
503 | — | Daemon connected but WebSocket disconnected |
504 | — | Command response timeout (exceeded 60s) |
